import React, {
  createContext,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import WebView from 'react-native-webview';
import customUUID from '../app/functions/customUUID';
import EventEmitter from 'events';
import { sha256 } from '@noble/hashes/sha2';
import { hkdf } from '@noble/hashes/hkdf';
import { AppState, Platform } from 'react-native';
import { getSharedSecret, getPublicKey } from '@noble/secp256k1';
import {
  createCipheriv,
  createDecipheriv,
  randomBytes,
} from 'react-native-quick-crypto';
import sha256Hash from '../app/functions/hash';
import { verifyAndPrepareWebView } from '../app/functions/webview/bundleVerification';
import DeviceInfo, {
  getModel,
  getSystemVersion,
} from 'react-native-device-info';
import { getLocalStorageItem, setLocalStorageItem } from '../app/functions';
import { useAppStatus } from './appStatus';
import { useActiveCustodyAccount } from './activeAccount';
import { useAuthContext } from './authContext';
import { navigationRef } from '../navigation/navigationService';

export const OPERATION_TYPES = {
  // Spark
  initWallet: 'initializeSparkWallet',
  getIdentityKey: 'getSparkIdentityPubKey',
  getBalance: 'getSparkBalance',
  getL1Address: 'getSparkStaticBitcoinL1Address',
  queryStaticL1Address: 'queryAllStaticDepositAddresses',
  getUtxosForDepositAddress: 'getUtxosForDepositAddress',
  getL1AddressQuote: 'getSparkStaticBitcoinL1AddressQuote',
  claimStaticDepositAddress: 'claimnSparkStaticDepositAddress',
  getSparkAddress: 'getSparkAddress',
  sendSparkPayment: 'sendSparkPayment',
  sendTokenPayment: 'sendSparkTokens',
  getLightningFee: 'getSparkLightningPaymentFeeEstimate',
  getBitcoinPaymentRequest: 'getSparkBitcoinPaymentRequest',
  getBitcoinPaymentFee: 'getSparkBitcoinPaymentFeeEstimate',
  getSparkPaymentFee: 'getSparkPaymentFeeEstimate',
  receiveLightningPayment: 'receiveSparkLightningPayment',
  getLightningSendRequest: 'getSparkLightningSendRequest',
  getLightningPaymentStatus: 'getSparkLightningPaymentStatus',
  sendLightningPayment: 'sendSparkLightningPayment',
  sendBitcoinPayment: 'sendSparkBitcoinPayment',
  getTransactions: 'getSparkTransactions',
  getTokenTransactions: 'getSparkTokenTransactions',
  addListeners: 'addWalletEventListener',
  removeListeners: 'removeWalletEventListener',
  disposeWallet: 'disposeSparkWallet',
  setPrivacyEnabled: 'setPrivacyEnabled',
  getSingleTxDetails: 'getSingleTxDetails',
  createSatsInvoice: 'createSatsInvoice',
  fufillSparkInvoices: 'fufillSparkInvoices',
  batchTransferTokens: 'batchTransferTokens',
  createTokensInvoice: 'createTokensInvoice',
  claimSparkHodlLightningPayment: 'claimSparkHodlLightningPayment',
  receiveSparkHodlLightningPayment: 'receiveSparkHodlLightningPayment',
  querySparkHodlLightningPayments: 'querySparkHodlLightningPayments',

  // Flashnet
  initializeFlashnet: 'initializeFlashnet',
  listFlashnetPools: 'listFlashnetPools',
  findBestPool: 'findBestPool',
  getPoolDetails: 'getPoolDetails',
  listAllPools: 'listAllPools',
  minFlashnetSwapAmounts: 'minFlashnetSwapAmounts',
  simulateSwap: 'simulateSwap',
  executeSwap: 'executeSwap',
  swapBitcoinToToken: 'swapBitcoinToToken',
  swapTokenToBitcoin: 'swapTokenToBitcoin',
  getLightningPaymentQuote: 'getLightningPaymentQuote',
  payLightningWithToken: 'payLightningWithToken',
  getUserSwapHistory: 'getUserSwapHistory',
  requestClawback: 'requestClawback',
  checkClawbackEligibility: 'checkClawbackEligibility',
  checkClawbackStatus: 'checkClawbackStatus',
  requestBatchClawback: 'requestBatchClawback',
  listClawbackableTransfers: 'listClawbackableTransfers',

  // Wallet optimizations
  abortOptimization: 'abortOptimization',
  isOptimizationRunning: 'isOptimizationRunning',
  checkIfOptimizationNeeded: 'checkIfOptimizationNeeded',
  runLeafOptimization: 'runLeafOptimization',
  runTokenOptimization: 'runTokenOptimization',
};

const longOperations = [
  OPERATION_TYPES.claimStaticDepositAddress,
  OPERATION_TYPES.sendSparkPayment,
  OPERATION_TYPES.sendTokenPayment,
  OPERATION_TYPES.getBitcoinPaymentRequest,
  OPERATION_TYPES.getBitcoinPaymentFee,
  OPERATION_TYPES.sendLightningPayment,
  OPERATION_TYPES.sendBitcoinPayment,
  OPERATION_TYPES.initWallet,
  OPERATION_TYPES.initializeFlashnet,
  OPERATION_TYPES.executeSwap,
  OPERATION_TYPES.swapBitcoinToToken,
  OPERATION_TYPES.swapTokenToBitcoin,
  OPERATION_TYPES.payLightningWithToken,
  OPERATION_TYPES.requestClawback,
  OPERATION_TYPES.runLeafOptimization,
  OPERATION_TYPES.runTokenOptimization,
  OPERATION_TYPES.claimSparkHodlLightningPayment,
  OPERATION_TYPES.receiveSparkHodlLightningPayment,
  OPERATION_TYPES.fufillSparkInvoices,
  OPERATION_TYPES.batchTransferTokens,
];

const mediumOperations = [
  OPERATION_TYPES.getBalance,
  OPERATION_TYPES.queryStaticL1Address,
  OPERATION_TYPES.getUtxosForDepositAddress,
  OPERATION_TYPES.getL1AddressQuote,
  OPERATION_TYPES.getSparkAddress,
  OPERATION_TYPES.getL1Address,
  OPERATION_TYPES.receiveLightningPayment,
  OPERATION_TYPES.getLightningSendRequest,
  OPERATION_TYPES.getLightningPaymentStatus,
  OPERATION_TYPES.getTransactions,
  OPERATION_TYPES.getSingleTxDetails,
  OPERATION_TYPES.getTokenTransactions,
  OPERATION_TYPES.setPrivacyEnabled,
  OPERATION_TYPES.simulateSwap,
  OPERATION_TYPES.requestBatchClawback,
  OPERATION_TYPES.checkIfOptimizationNeeded,
  OPERATION_TYPES.listClawbackableTransfers,
  OPERATION_TYPES.createSatsInvoice,
  OPERATION_TYPES.createTokensInvoice,
  OPERATION_TYPES.getLightningPaymentQuote,
  OPERATION_TYPES.getLightningFee,
  OPERATION_TYPES.getSparkPaymentFee,
  OPERATION_TYPES.getUserSwapHistory,
  OPERATION_TYPES.checkClawbackEligibility,
  OPERATION_TYPES.checkClawbackStatus,
];

const rejectIfNotConnectedToInternet = [
  OPERATION_TYPES.claimStaticDepositAddress,
  OPERATION_TYPES.sendSparkPayment,
  OPERATION_TYPES.sendTokenPayment,
  OPERATION_TYPES.getBitcoinPaymentRequest,
  OPERATION_TYPES.getBitcoinPaymentFee,
  OPERATION_TYPES.sendLightningPayment,
  OPERATION_TYPES.sendBitcoinPayment,
  OPERATION_TYPES.receiveLightningPayment,
  OPERATION_TYPES.getL1Address,
  OPERATION_TYPES.getSparkAddress,
];

export const INCOMING_SPARK_TX_NAME = 'RECEIVED_CONTACTS EVENT';
export const incomingSparkTransaction = new EventEmitter();

export const BALANCE_UPDATE_EVENT_NAME = 'SPARK_BALANCE_UPDATE';
export const sparkBalanceUpdateEmitter = new EventEmitter();

export const TOKEN_BALANCE_UPDATE_EVENT_NAME = 'SPARK_TOKEN_BALANCE_UPDATE';
export const sparkTokenBalanceUpdateEmitter = new EventEmitter();

export const STREAM_STATUS_EVENT_NAME = 'SPARK_STREAM_STATUS';
export const sparkStreamStatusEmitter = new EventEmitter();
const WASM_ERRORS = [
  'WASM',
  'WebAssembly',
  'WebAssembly.Compile is disallowed on the main thread',
  "Cannot read properties of undefined (reading '__wbindgen_malloc')",
];
let handshakeComplete = false;
let forceReactNativeUse = null;
let globalSendWebViewRequest = null;
let webviewFailureCount = 0;
const MAX_WEBVIEW_FAILURES = 2;

const WV_STATES = {
  UNLOADED: 'unloaded',
  VERIFYING: 'verifying',
  LOADING: 'loading',
  LOADED: 'loaded',
  HANDSHAKING: 'handshaking',
  READY: 'ready',
  ERROR: 'error',
};

const VALID_TRANSITIONS = {
  [WV_STATES.UNLOADED]: [WV_STATES.VERIFYING],
  [WV_STATES.VERIFYING]: [WV_STATES.LOADING, WV_STATES.ERROR],
  [WV_STATES.LOADING]: [WV_STATES.LOADED, WV_STATES.ERROR, WV_STATES.UNLOADED],
  [WV_STATES.LOADED]: [
    WV_STATES.HANDSHAKING,
    WV_STATES.ERROR,
    WV_STATES.UNLOADED,
  ],
  [WV_STATES.HANDSHAKING]: [
    WV_STATES.READY,
    WV_STATES.ERROR,
    WV_STATES.UNLOADED,
  ],
  [WV_STATES.READY]: [WV_STATES.ERROR, WV_STATES.UNLOADED],
  [WV_STATES.ERROR]: [WV_STATES.UNLOADED],
};

const WebViewContext = createContext(null);

// Derive AES-256 key via HKDF-SHA256 from sharedX (32 bytes)
function deriveAesKeyFromSharedX(sharedX, randomNonce) {
  // sharedX should be Uint8Array or Buffer
  const ikm =
    sharedX instanceof Uint8Array ? sharedX : Uint8Array.from(sharedX);
  // no salt, info = 'ecdh-aes-key'
  const keyBytes = hkdf(
    sha256,
    ikm,
    new Uint8Array(0),
    new TextEncoder().encode('ecdh-aes-key:' + randomNonce),
    32,
  );
  return Buffer.from(keyBytes); // Buffer of length 32
}

const setHandshakeComplete = value => {
  handshakeComplete = value;
};

export const setForceReactNative = (value, reason = 'unknown') => {
  if (value === true) {
    console.warn(`forceReactNativeUse set to true. Reason: ${reason}`);
  }
  forceReactNativeUse = value;
};

const forceNativeMode = reason => {
  console.warn(`Forcing React Native mode: ${reason}`);
  forceReactNativeUse = true;
};

export const sendWebViewRequestGlobal = async (
  action,
  args = {},
  encrypt = true,
) => {
  if (!globalSendWebViewRequest) {
    throw new Error(
      'WebView not initialized. Ensure WebViewProvider is mounted.',
    );
  }
  return globalSendWebViewRequest(action, args, encrypt);
};

export const getHandshakeComplete = () => {
  if (forceReactNativeUse !== null) {
    return false;
  }
  return handshakeComplete;
};

export const WebViewProvider = ({ children }) => {
  const { authResetkey } = useAuthContext();
  const { currentWalletMnemoinc } = useActiveCustodyAccount();
  const { appState, isConnectedToTheInternet, didGetToHomepage } =
    useAppStatus();
  const webViewRef = useRef(null);
  const [isWebViewReady, setIsWebViewReady] = useState(false);
  const [reloadKey, setReloadKey] = useState(0);
  const isResetting = useRef(false);
  const queuedRequests = useRef([]);
  const pendingRequests = useRef({});
  const activeTimeoutsRef = useRef({});
  const sessionKeyRef = useRef(null);
  const aesKeyRef = useRef(null);
  const expectedNonceRef = useRef(null);
  const [verifiedPath, setVerifiedPath] = useState('');
  const expectedSequenceRef = useRef(0);
  const nonceVerified = useRef(false);
  const previousAppState = useRef(appState);
  const prevConnectionStatus = useRef(isConnectedToTheInternet);
  const internetConnectionRef = useRef(isConnectedToTheInternet);
  const walletInitialized = useRef(false);
  const isInitialRender = useRef(true);
  const currentWalletMnemoincRef = useRef(currentWalletMnemoinc);
  const didRunInit = useRef(null);
  const isWebviewReadyRef = useRef(null);
  const didRunHandshakeRef = useRef(false);
  const didGetToHomepageRef = useRef(didGetToHomepage);
  const [changeSparkConnectionState, setChangeSparkConnectionState] = useState({
    state: null,
    count: 0,
  });
  const webViewLoadState = useRef('unloaded');
  const onLoadEndCalledRef = useRef(false);
  const lastLoadEndTimeRef = useRef(0);
  const wvState = useRef(WV_STATES.UNLOADED);

  const transitionWvState = useCallback((newState, reason = '') => {
    const current = wvState.current;
    const valid = VALID_TRANSITIONS[current];
    if (!valid || !valid.includes(newState)) {
      console.warn(
        `Invalid WebView state transition: ${current} → ${newState} (reason: ${reason})`,
      );
      return false;
    }
    console.log(`WebView state: ${current} → ${newState} (${reason})`);
    wvState.current = newState;

    // Derive isWebViewReady from state machine
    const ready =
      newState === WV_STATES.LOADED ||
      newState === WV_STATES.HANDSHAKING ||
      newState === WV_STATES.READY;
    setIsWebViewReady(ready);
    return true;
  }, []);

  const fileHash = !!verifiedPath ? process.env.WEBVIEW_BUNDLE_HASH : '';

  useEffect(() => {
    currentWalletMnemoincRef.current = currentWalletMnemoinc;
  }, [currentWalletMnemoinc]);

  useEffect(() => {
    isWebviewReadyRef.current = isWebViewReady;
  }, [isWebViewReady]);

  // reset webview when app is stale in background
  useEffect(() => {
    if (isInitialRender.current) {
      isInitialRender.current = false;
      return;
    }

    // clear any qued requests
    if (queuedRequests.current.length) {
      console.log('[auth reset webview] clearing qued requests');
      // Snapshot and clear BEFORE iterating: a reject handler can synchronously
      // route back into a queue drain (see wrapped init resolve), so the array
      // must already be empty to keep that re-entry from recursing infinitely.
      const requestsToReject = queuedRequests.current;
      queuedRequests.current = [];
      requestsToReject.forEach(({ reject }) => {
        reject({
          error: 'Wallet initialization failed, using React Native',
        });
      });
    }
    blockAndResetWebview();
  }, [authResetkey]);

  useEffect(() => {
    didGetToHomepageRef.current = didGetToHomepage;
  }, [didGetToHomepage]);

  useEffect(() => {
    internetConnectionRef.current = isConnectedToTheInternet;
  }, [isConnectedToTheInternet]);

  const messageRateLimiter = useRef({
    count: 0,
    windowStart: Date.now(),
    maxPerSecond: 50,
  });

  const blockAndResetWebview = useCallback(
    shouldClearPending => {
      didRunInit.current = true; // Block handshakes during reload

      resetWebViewState(false, false, shouldClearPending);
      reloadWebViewSecurely(); // Will allow handshake to complete after state variables change. We are preventing a race condition here with the app state.
    },
    [resetWebViewState, reloadWebViewSecurely],
  );

  const getNextSequence = useCallback(() => {
    const current = expectedSequenceRef.current;
    expectedSequenceRef.current = current + 1;
    return current;
  }, []);

  const isDuplicate = (queue, action, args) => {
    return queue.some(
      req =>
        ![
          OPERATION_TYPES.getSingleTxDetails,
          OPERATION_TYPES.getUserSwapHistory,
        ].includes(req.action) &&
        req.action === action &&
        JSON.stringify(req.args) === JSON.stringify(args),
    );
  };

  const resetWebViewState = useCallback(
    (
      clearHandshake = false,
      sparkConnectionState,
      shouldClearPending = true,
    ) => {
      console.log('Resetting WebView state', {
        clearHandshake,
        sparkConnectionState,
        shouldClearPending,
      });
      // Transition to UNLOADED — this also sets isWebViewReady(false) via derived state.
      // Allow transition from any state during reset.
      wvState.current = WV_STATES.UNLOADED;
      isResetting.current = true;
      setIsWebViewReady(false);
      // setVerifiedPath('');

      Object.entries(activeTimeoutsRef.current).forEach(([id, timeoutInfo]) => {
        clearTimeout(timeoutInfo.timeoutId);

        // Re-queue the request if it's not a handshake
        // and if we're not clearing pending requests
        if (!shouldClearPending && timeoutInfo.action !== 'handshake:init') {
          // Find the pending request's resolve/reject functions
          const originalResolve = pendingRequests.current[id];
          if (originalResolve && timeoutInfo.originalRequest) {
            const { action, args, encrypt } = timeoutInfo.originalRequest;

            console.log(`Re-queueing interrupted request: ${action}`);

            // Add to queue if not already there
            // IMPORTANT: We pass the original resolve function so the promise chain is preserved
            if (!isDuplicate(queuedRequests.current, action, args)) {
              queuedRequests.current.push({
                id, // Keep original ID for tracking
                action,
                args,
                encrypt,
                resolve: originalResolve,
                reject: error => {
                  // Reject using the original resolve function (which handles both resolve/reject)
                  if (typeof originalResolve === 'function') {
                    originalResolve({ error: error.message || error });
                  }
                },
              });
            }

            // Remove from pendingRequests since it's now queued
            delete pendingRequests.current[id];
          }
        }
      });
      activeTimeoutsRef.current = {};

      // Only clear handshake if explicitly told to (actual failures)
      // Don't clear it for normal app lifecycle resets
      // Our app uses this to choose wheater to make calls to the webview or react native
      if (clearHandshake) {
        setHandshakeComplete(false);
      }

      // Always reset walletInitialized because WebView reload clears its internal state
      // We'll need to reinitialize the wallet after handshake completes
      walletInitialized.current = false;
      setChangeSparkConnectionState(prev => ({
        state: sparkConnectionState,
        count: prev.count + 1,
      }));

      if (shouldClearPending) {
        Object.entries(pendingRequests.current).forEach(([id, resolve]) => {
          // Call the resolve to trigger timeout cleanup
          if (typeof resolve === 'function') {
            resolve({
              error: 'Unable to finish action, request got cleaned up.',
            });
          }
        });
        pendingRequests.current = {};
      }
      // Note: We already removed re-queued items from pendingRequests in the loop above,
      // so no additional cleanup needed in the else branch

      sessionKeyRef.current = null;
      expectedSequenceRef.current = 0;
      aesKeyRef.current = null;
      nonceVerified.current = false;
    },
    [],
  );

  const reloadWebViewSecurely = useCallback(async () => {
    try {
      console.log('Re-verifying WebView before reload...');
      if (forceReactNativeUse) return;

      transitionWvState(WV_STATES.VERIFYING, 'reload verification');

      // Re-verify the file
      const { htmlPath, nonceHex, hashHex } = await verifyAndPrepareWebView(
        Platform.OS === 'ios'
          ? require('spark-web-context')
          : 'file:///android_asset/sparkContext.html',
      );

      // File is verified, safe to reload
      console.log('File integrity verified, reloading WebView');
      didRunInit.current = false;
      expectedNonceRef.current = nonceHex;
      setVerifiedPath(htmlPath);
      setReloadKey(prev => prev + 1);
    } catch (err) {
      console.error('WebView re-verification failed:', err);

      // On verification failure, force React Native mode
      forceNativeMode('bundle verification failed');
      setHandshakeComplete(false);
      const currentRoutes = navigationRef
        .getRootState()
        .routes?.map(r => r.name);

      const blockReset =
        currentRoutes?.includes('Splash') ||
        currentRoutes?.includes('SplashReload') ||
        currentRoutes?.includes('Home') ||
        currentRoutes?.includes('ConnectingToNodeLoadingScreen');

      setChangeSparkConnectionState(prev => ({
        state: blockReset ? null : true,
        count: prev.count + 1,
      }));
    }
  }, []);

  // Handle app state changes
  useEffect(() => {
    const appStateChanged = previousAppState.current !== appState;
    const connectionChanged =
      prevConnectionStatus.current !== isConnectedToTheInternet;

    if ((!appStateChanged && !connectionChanged) || forceReactNativeUse) {
      return; // Nothing changed
    }

    if (appState === 'background') {
      console.log(
        'App going to background - clearing all timeouts and pending requests',
      );
      Object.values(activeTimeoutsRef.current).forEach(t =>
        clearTimeout(t.timeoutId),
      );
      activeTimeoutsRef.current = {};

      previousAppState.current = appState;
      prevConnectionStatus.current = isConnectedToTheInternet;
    } else if (appState === 'active') {
      console.log('App returned to foreground');
      // clear any active timeouts to prevent timeout from switching to rn
      Object.values(activeTimeoutsRef.current).forEach(t =>
        clearTimeout(t.timeoutId),
      );

      // Wait for internet connection before proceeding
      if (!isConnectedToTheInternet) {
        console.log('Waiting for internet connection before processing...');
        // Update refs so we can detect when connection comes back
        previousAppState.current = appState;
        prevConnectionStatus.current = isConnectedToTheInternet;

        return;
      }

      // Only execute if we actually transitioned to active OR connection just came back
      const justBecameActive =
        appStateChanged &&
        (previousAppState.current === 'background' ||
          previousAppState.current === 'inactive');
      const connectionJustRestored =
        connectionChanged && isConnectedToTheInternet;

      if (justBecameActive || connectionJustRestored) {
        if (!nonceVerified.current && !isResetting.current) {
          console.log(
            'App became active or connection restored and webview is not varified and not resetting - reloading WebView',
          );
          if (didGetToHomepageRef.current) {
            blockAndResetWebview();
          }
        } else {
          // sometimes the webview becomes stale, if internet connection goes away make sure to reset webview but dont clear pending events so they are handled once webview is active again
          if (connectionJustRestored) {
            blockAndResetWebview(false);
          } else {
            // Make sure to handle any events that happen during background and are within the three minute refresh timeout
            if (!didGetToHomepageRef.current) {
              // we need to make sure this doesn't double run before getting to the hompage otherwise we will send multiple init requests
              console.log(
                'Did not get to homepage yet, blocking duplicate request created by biometric login popup',
              );
            } else {
              setTimeout(() => {
                processQueuedRequests(connectionJustRestored);
              }, 100);
            }
          }
        }
      }

      previousAppState.current = appState;
      prevConnectionStatus.current = isConnectedToTheInternet;
    } else {
      previousAppState.current = appState;
      prevConnectionStatus.current = isConnectedToTheInternet;
    }
  }, [
    appState,
    isConnectedToTheInternet,
    blockAndResetWebview,
    processQueuedRequests,
  ]);

  const encryptMessage = useCallback(plaintext => {
    if (!aesKeyRef.current) throw new Error('AES key not initialized');
    const iv = Buffer.from(randomBytes(12));
    const cipher = createCipheriv('aes-256-gcm', aesKeyRef.current, iv);
    let encrypted = cipher.update(plaintext, 'utf8', 'base64');
    encrypted += cipher.final('base64');
    const authTag = cipher.getAuthTag().toString('base64'); // Get 16-byte auth tag
    return `${encrypted}?iv=${iv.toString('base64')}&tag=${authTag}`;
  }, []);

  const decryptMessage = useCallback(encryptedText => {
    if (!aesKeyRef.current) throw new Error('AES key not initialized');
    if (!encryptedText.includes('?iv=') || !encryptedText.includes('&tag=')) {
      throw new Error('Missing IV or auth tag');
    }
    const [ciphertext, params] = encryptedText.split('?iv=');
    const [ivBase64, authTagBase64] = params.split('&tag=');
    const iv = Buffer.from(ivBase64, 'base64');
    const authTag = Buffer.from(authTagBase64, 'base64');

    const decipher = createDecipheriv('aes-256-gcm', aesKeyRef.current, iv);
    decipher.setAuthTag(authTag); // Set auth tag for verification
    let decrypted = decipher.update(ciphertext, 'base64', 'utf8');
    decrypted += decipher.final('utf8');
    return decrypted;
  }, []);

  const handleWebViewResponse = useCallback(
    event => {
      try {
        const message = JSON.parse(event.nativeEvent.data);

        if (message.type === 'handshake:reply' && message.pubW) {
          const resolve = pendingRequests.current[message.id];
          if (!resolve) {
            // no need to handle anything here, will be handled with timeout
            console.error('Timeout: backend is unresponsive');
            return;
          }
          if (!sessionKeyRef.current) {
            // no need to handle anything here, will be handled with timeout
            console.error(
              'SECURITY: Received handshake reply without active session key',
            );
            return;
          }

          const shared = getSharedSecret(
            Buffer.from(sessionKeyRef.current.privateKey),
            Buffer.from(message.pubW, 'hex'),
            true,
          );
          const sharedX = shared.slice(1, 33);
          aesKeyRef.current = deriveAesKeyFromSharedX(
            sharedX,
            expectedNonceRef.current,
          );

          shared.fill(0);
          sharedX.fill(0);

          if (sessionKeyRef.current?.privateKey) {
            sessionKeyRef.current.privateKey.fill(0);
          }
          sessionKeyRef.current = null;

          const decodedNonce = decryptMessage(message.runtimeNonce);
          if (expectedNonceRef.current !== decodedNonce) {
            // no need to handle anything here, will be handled with timeout
            console.log('Invalid runtime nonce, something went wrong');
            aesKeyRef.current = null;
            return;
          }
          nonceVerified.current = true;
          webviewFailureCount = 0;
          console.log('Handshake complete. Got backend public key.');
          transitionWvState(WV_STATES.READY, 'handshake complete');
          setHandshakeComplete(true);
          // resolve requset to avoid timeout
          resolve({ didComplete: true });
          delete pendingRequests.current[message.id];

          setTimeout(() => {
            processQueuedRequests();
          }, 100);
          return;
        }

        let content = message;

        if (message.encrypted && aesKeyRef.current) {
          const decrypted = decryptMessage(message.encrypted);

          try {
            content = JSON.parse(decrypted);
          } catch (err) {
            content = decrypted;
          }
        }
        console.log('receiving message from webview', content);

        if (content.type === 'security:csp-violation') {
          console.error('CSP VIOLATION DETECTED:', content);

          resetWebViewState(true, true);
          forceNativeMode('CSP violation');
          return;
        }

        // Unsolicited SDK push events (incoming payment, balance/token balance,
        // stream status) are not request/response traffic and must not count
        // toward the flood limiter. A burst of legitimate inbound payments would
        // otherwise trip it and force the one-way native fallback.
        const isSdkPushEvent = !!(
          content.incomingPayment ||
          content.balanceUpdate ||
          content.tokenBalanceUpdate ||
          content.streamStatus
        );

        if (!isSdkPushEvent) {
          const now = Date.now();
          const windowDuration = now - messageRateLimiter.current.windowStart;
          if (windowDuration > 1000) {
            // Reset window
            messageRateLimiter.current.count = 0;
            messageRateLimiter.current.windowStart = now;
          }
          messageRateLimiter.current.count++;

          if (
            messageRateLimiter.current.count >
            messageRateLimiter.current.maxPerSecond
          ) {
            console.error(
              `SECURITY: Rate limit exceeded (${messageRateLimiter.current.count} msgs/sec)`,
            );

            resetWebViewState(true, true);
            forceNativeMode('rate limit exceeded');
            return;
          }
        }

        if (content.error) throw new Error(content.error);

        if (content.incomingPayment) {
          const data = JSON.parse(content.result);
          incomingSparkTransaction.emit(
            INCOMING_SPARK_TX_NAME,
            data.transferId,
            data.balance,
            content.walletId,
          );
        }
        if (content.balanceUpdate) {
          const data = JSON.parse(content.result);
          sparkBalanceUpdateEmitter.emit(
            BALANCE_UPDATE_EVENT_NAME,
            data,
            content.walletId,
          );
        }
        if (content.tokenBalanceUpdate) {
          const data = JSON.parse(content.result);
          sparkTokenBalanceUpdateEmitter.emit(
            TOKEN_BALANCE_UPDATE_EVENT_NAME,
            data.tokensObject,
            content.walletId,
          );
        }
        if (content.streamStatus) {
          sparkStreamStatusEmitter.emit(
            STREAM_STATUS_EVENT_NAME,
            content.streamStatus,
          );
        }
        if (content.isResponse && content.id) {
          const resolve = pendingRequests.current[content.id];
          if (resolve) {
            const result = JSON.parse(content.result || null);
            // Check for WASM errors
            if (
              result?.error &&
              typeof result.error === 'string' &&
              WASM_ERRORS.some(errMsg => result.error.includes(errMsg))
            ) {
              console.warn(
                'WASM failed, switching to React Native implementation:',
                result.error,
              );

              resetWebViewState(true, true);
              forceNativeMode('WASM error');

              setLocalStorageItem('FORCE_REACT_NATIVE', 'true');
            }
            webviewFailureCount = 0; // Reset on successful response
            resolve(result);

            delete pendingRequests.current[content.id];
          }
        }
      } catch (err) {
        console.error('Error handling WebView message:', err);
        if (
          typeof err.message === 'string' &&
          err.message === 'Rejected stale message'
        )
          return;
        webviewFailureCount++;
        if (webviewFailureCount >= MAX_WEBVIEW_FAILURES) {
          forceNativeMode('repeated WebView errors');
        }
        resetWebViewState(true, true);
      }
    },
    [decryptMessage, resetWebViewState],
  );

  const sendWebViewRequestInternal = useCallback(
    (action, args = {}, encrypt = true) => {
      let resolveFn, rejectFn;
      const promise = new Promise((resolve, reject) => {
        resolveFn = resolve;
        rejectFn = reject;
      });

      const execute = async () => {
        const resolve = resolveFn;
        const reject = rejectFn;
        let timeoutId = null;
        const id = customUUID();
        try {
          // If forceReactNativeUse is set, reject immediately
          if (forceReactNativeUse === true) {
            console.log(
              'Forced React Native mode, rejecting WebView request:',
              action,
            );
            return resolve({
              error: 'Wallet initialization failed, using React Native(1)',
            });
          }

          // Queue messages during reset/background
          if (
            (isResetting.current || AppState.currentState === 'background') &&
            action !== 'handshake:init' &&
            action !== 'initializeSparkWallet'
          ) {
            console.log(
              'WebView is resetting or in the background, queueing message:',
              action,
            );
            if (!isDuplicate(queuedRequests.current, action, args)) {
              queuedRequests.current.push({
                id,
                action,
                args,
                encrypt,
                resolve,
                reject,
              });
            } else {
              console.log('Duplicate request ignored:', action, args);
              reject('Duplicate request ignored');
            }
            return;
          }

          // Reject importent messages if app is not connected to the internet
          if (
            !internetConnectionRef.current &&
            action !== 'handshake:init' &&
            action !== 'initializeSparkWallet'
          ) {
            console.log(
              'App is not connected to the internet, queueing message:',
              action,
            );
            if (
              rejectIfNotConnectedToInternet.some(op =>
                action.toLowerCase().includes(op.toLowerCase()),
              )
            ) {
              reject(new Error(`App is not connected to the internet`));
            } else if (action !== OPERATION_TYPES.initWallet) {
              //don't add init wallet, this will be added during webview reset
              if (!isDuplicate(queuedRequests.current, action, args)) {
                queuedRequests.current.push({
                  id,
                  action,
                  args,
                  encrypt,
                  resolve,
                  reject,
                });
              } else {
                console.log('Duplicate request ignored:', action, args);
                reject('Duplicate request ignored');
              }
            }
            return;
          }

          if (!webViewRef.current || !isWebviewReadyRef.current) {
            console.log(
              'WebView not ready or internet is not connected, queueing message:',
              action,
              webViewRef.current,
              isWebviewReadyRef.current,
            );
            if (!isDuplicate(queuedRequests.current, action, args)) {
              queuedRequests.current.push({
                id,
                action,
                args,
                encrypt,
                resolve,
                reject,
              });
            } else {
              console.log('Duplicate request ignored:', action, args);
              reject('Duplicate request ignored');
            }
            return;
          }

          const getTimeoutDuration = action => {
            if (action === 'handshake:init') return 4000;

            if (
              longOperations.some(op =>
                action.toLowerCase().includes(op.toLowerCase()),
              )
            ) {
              return 90000; // 90 seconds for payment operations
            }

            if (
              mediumOperations.some(op =>
                action.toLowerCase().includes(op.toLowerCase()),
              )
            ) {
              return 30000; // 30 seconds
            }

            return 10000; // 10 seconds
          };

          const timeoutDuration = getTimeoutDuration(action);
          const startedAt = Date.now();

          const handleTimeout = () => {
            if (AppState.currentState !== 'active') {
              console.log(
                `Skipping timeout for ${action} because app is not active (${AppState.currentState})`,
              );
              return;
            }

            if (!pendingRequests.current[id]) {
              console.log(`Request ${id} already resolved, skipping timeout`);
              delete activeTimeoutsRef.current[id];
              return;
            }

            console.error(`WebView request timeout for action: ${action}`);

            delete pendingRequests.current[id];
            delete activeTimeoutsRef.current[id];

            // If handshake is complete and this is not a handshake action,
            // the WebView bridge is functional — this is a service timeout.
            // Don't kill the WebView for a backend service being slow.
            if (handshakeComplete && action !== 'handshake:init') {
              console.warn(
                `Service timeout for ${action} — WebView bridge is healthy, not forcing native mode`,
              );
            } else {
              // WebView-level failure (handshake timeout or bridge issue)
              webviewFailureCount++;
              console.warn(
                `WebView failure #${webviewFailureCount} for ${action}`,
              );
              if (webviewFailureCount >= MAX_WEBVIEW_FAILURES) {
                forceNativeMode(
                  `${MAX_WEBVIEW_FAILURES} consecutive WebView failures`,
                );
              }
              resetWebViewState(true, true);
            }

            reject(
              new Error(
                `Call unresponsive (timeout after ${timeoutDuration}ms)`,
              ),
            );
          };

          timeoutId = setTimeout(handleTimeout, timeoutDuration);

          activeTimeoutsRef.current[id] = {
            timeoutId,
            startedAt,
            duration: timeoutDuration,
            handler: handleTimeout,
            remaining: timeoutDuration,
            action,
            originalRequest: { action, args, encrypt }, //Store request details
          };

          const originalResolve = resolve;
          pendingRequests.current[id] = result => {
            const t = activeTimeoutsRef.current[id];
            if (t?.timeoutId) clearTimeout(t.timeoutId);
            delete activeTimeoutsRef.current[id];
            originalResolve(result);
          };

          if (args.mnemonic && action !== 'initializeSparkWallet') {
            args.mnemonic = sha256Hash(args.mnemonic);
          }

          // Handle initializeSparkWallet specially
          if (action === 'initializeSparkWallet') {
            if (!getHandshakeComplete()) {
              console.log('Handshake not complete, cannot initialize wallet');
              if (timeoutId) clearTimeout(timeoutId);
              delete pendingRequests.current[id];
              delete activeTimeoutsRef.current[id];
              forceNativeMode('handshake incomplete for wallet init');
              setChangeSparkConnectionState(prev => ({
                state: true,
                count: prev.count + 1,
              }));
              return resolve({ isConnected: false });
            }
            if (!nonceVerified.current) {
              console.log('Nonce not verified, cannot initialize wallet');
              if (timeoutId) clearTimeout(timeoutId);
              delete pendingRequests.current[id];
              delete activeTimeoutsRef.current[id];
              forceNativeMode('nonce not verified for wallet init');
              setChangeSparkConnectionState(prev => ({
                state: true,
                count: prev.count + 1,
              }));
              return resolve({ isConnected: false });
            }

            // Wrap the resolve to check initialization result
            const wrappedResolve = pendingRequests.current[id];
            pendingRequests.current[id] = result => {
              if (result?.error || result?.isConnected === false) {
                console.warn(
                  'Wallet initialization failed, forcing React Native mode:',
                  result,
                );

                // forceReactNativeUse = true;
                // setChangeSparkConnectionState(prev => ({
                //   state: true,
                //   count: prev.count + 1,
                // }));

                // Snapshot and clear BEFORE iterating. This forEach is the
                // re-entrant frame in the crash: one of these rejects belongs to
                // a re-queued init request whose reject calls THIS wrapped resolve
                // again, which would re-enter this forEach on the same array.
                // Clearing first makes that re-entry a no-op and breaks the loop.
                const requestsToReject = queuedRequests.current;
                queuedRequests.current = [];
                requestsToReject.forEach(({ reject }) => {
                  reject({
                    error: 'Wallet initialization failed, using React Native',
                  });
                });
              } else {
                walletInitialized.current = true;
                setChangeSparkConnectionState(prev => ({
                  state: true,
                  count: prev.count + 1,
                }));
                console.log('Wallet initialized successfully');
                processQueuedRequests();
              }
              wrappedResolve(result);
            };
          } else if (action !== 'handshake:init') {
            // For non-init actions, check if wallet was initialized
            if (handshakeComplete && !walletInitialized.current) {
              console.log(
                'Wallet initialization in progress, queueing request:',
                action,
              );

              // Queue the request instead of blocking
              if (!isDuplicate(queuedRequests.current, action, args)) {
                queuedRequests.current.push({
                  id,
                  action,
                  args,
                  encrypt,
                  resolve,
                  reject,
                });

                // Clean up timeout since we're queueing
                if (timeoutId) clearTimeout(timeoutId);
                delete activeTimeoutsRef.current[id];

                return;
              } else {
                console.log('Duplicate request ignored:', action);
                if (timeoutId) clearTimeout(timeoutId);
                delete pendingRequests.current[id];
                delete activeTimeoutsRef.current[id];
                return reject(new Error('Duplicate request ignored'));
              }
            }
          }

          const sequence = getNextSequence();
          const timestamp = Date.now();

          let payload = {
            id,
            action,
            args,
            sequence,
            timestamp,
          };
          console.log('sending message to webview', action, payload);

          try {
            if (encrypt && aesKeyRef.current) {
              const encrypted = encryptMessage(JSON.stringify(payload));
              payload = { type: 'secure:msg', encrypted };
            }
            webViewRef.current.postMessage(JSON.stringify(payload));
          } catch (err) {
            if (timeoutId) clearTimeout(timeoutId);
            delete pendingRequests.current[id];
            delete activeTimeoutsRef.current[id];
            reject(err);
          }
        } catch (err) {
          // Clean up timeout on error
          if (timeoutId) {
            clearTimeout(timeoutId);
            timeoutId = null;
          }
          if (id) {
            delete pendingRequests.current[id];
            delete activeTimeoutsRef.current[id];
          }
          console.log(
            'Error sending webview request from internal function',
            err,
          );
          reject(err);
        }
      };
      execute();
      return promise;
    },
    [
      encryptMessage,
      resetWebViewState,
      getNextSequence,
      appState,
      isConnectedToTheInternet,
    ],
  );

  const initHandshake = useCallback(async () => {
    try {
      const privN = randomBytes(32);
      const pubN = getPublicKey(privN, true); // compressed
      const pubNHex = Buffer.from(pubN).toString('hex');

      sessionKeyRef.current = {
        privateKey: privN,
        publicKey: pubNHex,
      };

      await sendWebViewRequestInternal('handshake:init', {
        pubN: pubNHex,
      });
    } catch (error) {
      console.warn('Handshake failed or timed out:', error.message);
      forceNativeMode('handshake failed');
      const currentRoutes = navigationRef
        .getRootState()
        .routes?.map(r => r.name);

      const blockReset =
        currentRoutes?.includes('Splash') ||
        currentRoutes?.includes('SplashReload') ||
        currentRoutes?.includes('Home') ||
        currentRoutes?.includes('ConnectingToNodeLoadingScreen');

      setChangeSparkConnectionState(prev => ({
        state: blockReset ? null : true,
        count: prev.count + 1,
      }));
      // Snapshot and clear BEFORE iterating (re-entrancy safety). This site also
      // previously never cleared the queue, leaving zombie requests behind.
      const requestsToReject = queuedRequests.current;
      queuedRequests.current = [];
      requestsToReject.forEach(({ reject }) => {
        reject({
          error: 'Failed to process method, try again',
        });
      });
    }
  }, [sendWebViewRequestInternal]);

  useEffect(() => {
    async function startHandshake() {
      if (!webViewRef.current) return;
      if (!isWebViewReady) return;
      if (!verifiedPath) return;
      // blocking background init event from firing
      if (appState === 'background') return;
      if (didRunInit.current) return;
      didRunInit.current = true;

      // const androidAPI = DeviceInfo.getApiLevelSync();
      // if (androidAPI == 33 || androidAPI == 34) {
      //   console.warn(`Skipping handshake on Android API ${androidAPI}`);
      //   forceReactNativeUse = true;
      //   return;
      // }

      const savedVariable = await getLocalStorageItem('FORCE_REACT_NATIVE');

      if (savedVariable === 'true') {
        console.log('FORCE_REACT_NATIVE is set, skipping handshake');
        forceNativeMode('FORCE_REACT_NATIVE localStorage flag');
        didRunHandshakeRef.current = true;
        return;
      }
      transitionWvState(WV_STATES.HANDSHAKING, 'handshake init');
      await initHandshake();
      didRunHandshakeRef.current = true;
    }

    const debouceID = setTimeout(() => {
      // forceReactNativeUse = true;
      // didRunHandshakeRef.current = true
      startHandshake(); //remove this and app fully uses RN
    }, 250);

    return () => {
      if (debouceID) {
        clearTimeout(debouceID);
      }
    };
  }, [isWebViewReady, verifiedPath, initHandshake, appState]);

  useEffect(() => {
    (async () => {
      transitionWvState(WV_STATES.VERIFYING, 'initial verification');
      try {
        const { htmlPath, nonceHex, hashHex } = await verifyAndPrepareWebView(
          Platform.OS === 'ios'
            ? require('spark-web-context')
            : 'file:///android_asset/sparkContext.html',
        );

        expectedNonceRef.current = nonceHex;
        setVerifiedPath(htmlPath);
      } catch (err) {
        didRunHandshakeRef.current = true;
        forceNativeMode('bundle verification failed');
        console.log(
          'WebView bundle verification failed. Using react-native bundle',
          err,
        );
      }
    })();
  }, []);

  useEffect(() => {
    globalSendWebViewRequest = sendWebViewRequestInternal;
  }, [sendWebViewRequestInternal]);

  useEffect(() => {
    webViewLoadState.current = 'unloaded';
    onLoadEndCalledRef.current = false;
    lastLoadEndTimeRef.current = 0;
  }, [reloadKey, verifiedPath]);

  const processQueuedRequests = useCallback(
    async connectionJustRestored => {
      // After a soft reset, the WebView's internal state is cleared
      // We must reinitialize the wallet before processing any queued requests
      if (
        (handshakeComplete &&
          !walletInitialized.current &&
          currentWalletMnemoincRef.current) ||
        (currentWalletMnemoincRef.current && connectionJustRestored)
      ) {
        console.log('Re-initializing wallet before processing queue');
        try {
          // No need to handle any state changes here, handled inside of the promise. But this might be where the stale connection state comes from. if a request is sent to the webview but not responded to the change to react-native woudnt have happpened before leaving everything in "not connected to spark".
          const response = await sendWebViewRequestInternal(
            OPERATION_TYPES.initWallet,
            { mnemonic: currentWalletMnemoincRef.current },
            true,
          );
          if (!response?.isConnected) throw new Error('Wallet init failed');
        } catch (err) {
          console.log('Error re-initializing wallet:', err);
          // forceReactNativeUse = true;
          // Reject all queued requests since WebView is now unusable
          // Snapshot and clear BEFORE iterating (re-entrancy safety).
          const requestsToReject = queuedRequests.current;
          queuedRequests.current = [];
          requestsToReject.forEach(({ reject }) => {
            reject({
              error: 'Wallet initialization failed, using React Native',
            });
          });
          return;
        }
      }
      isResetting.current = false;

      console.log(
        `Processing ${queuedRequests.current.length} queued requests`,
      );

      if (
        queuedRequests.current.length === 0 ||
        !currentWalletMnemoincRef.current
      ) {
        isResetting.current = false;
        return;
      }

      const requests = [...queuedRequests.current];
      queuedRequests.current = [];

      // Process sequentially to avoid triggering the rate limiter (50 msgs/sec).
      // Parallel dispatch via Promise.allSettled could fire 51+ messages at once,
      // permanently killing the WebView.
      for (const { action, args, encrypt, resolve, reject } of requests) {
        try {
          const result = await sendWebViewRequestInternal(
            action,
            args,
            encrypt,
          );
          if (typeof resolve === 'function') {
            resolve(result);
          }
        } catch (error) {
          if (typeof reject === 'function') {
            reject(error);
          }
        }
      }

      isResetting.current = false;
    },
    [sendWebViewRequestInternal],
  );

  const getCustomUserAgent = useCallback(() => {
    const deviceModel = getModel();
    const systemVersion = getSystemVersion();

    // For Android
    if (Platform.OS === 'android') {
      return `Mozilla/5.0 (Linux; Android ${systemVersion}; ${deviceModel}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Mobile Safari/537.36`;
    }

    // For iOS
    return `Mozilla/5.0 (iPhone; CPU iPhone OS ${systemVersion.replace(
      /\./g,
      '_',
    )} like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1`;
  }, []);

  const handleWebViewTermination = useCallback(
    reason => {
      transitionWvState(WV_STATES.ERROR, reason);

      if (AppState.currentState !== 'active') {
        // App is backgrounded — do NOT reload now (no events should fire in background).
        // Invalidate session so the foreground app-state effect sees !nonceVerified
        // and triggers blockAndResetWebview() cleanly when the user returns.
        // Critically: do NOT set isResetting.current = true here, as that would
        // block the foreground handler's !isResetting.current guard.
        console.warn(
          `[WebView] Crash in background (${reason}) — deferring reload to foreground`,
        );
        nonceVerified.current = false;
        aesKeyRef.current = null;
        sessionKeyRef.current = null;
        walletInitialized.current = false;
        return;
      }

      console.warn(`[WebView] Crash while active (${reason}) — reloading now`);
      blockAndResetWebview();
    },
    [blockAndResetWebview],
  );

  const providerValues = useMemo(() => {
    return {
      webViewRef,
      sendWebViewRequest: sendWebViewRequestInternal,
      fileHash,
      changeSparkConnectionState,
      didRunHandshakeRef,
    };
  }, [
    webViewRef,
    sendWebViewRequestInternal,
    fileHash,
    changeSparkConnectionState,
    didRunHandshakeRef,
  ]);

  return (
    <WebViewContext.Provider value={providerValues}>
      {children}
      {verifiedPath && (
        <WebView
          key={reloadKey}
          domStorageEnabled={true}
          allowFileAccess={true}
          allowFileAccessFromFileURLs={false}
          allowUniversalAccessFromFileURLs={false}
          thirdPartyCookiesEnabled={true}
          sharedCookiesEnabled={true}
          incognito={false}
          userAgent={getCustomUserAgent()}
          webviewDebuggingEnabled={false}
          cacheEnabled={false}
          mixedContentMode="never"
          javaScriptEnabled
          ref={webViewRef}
          containerStyle={{ position: 'absolute', top: 1000, left: 1000 }}
          source={{ uri: verifiedPath }}
          originWhitelist={['file://']}
          onShouldStartLoadWithRequest={request => {
            return request.url === verifiedPath;
          }}
          onMessage={handleWebViewResponse}
          onLoadStart={() => {
            transitionWvState(WV_STATES.LOADING, 'onLoadStart');
            webViewLoadState.current = 'loading';
            onLoadEndCalledRef.current = false;
            didRunHandshakeRef.current = false;
          }}
          onLoadProgress={({ nativeEvent }) => {
            if (
              nativeEvent.progress === 1 &&
              wvState.current === WV_STATES.LOADING
            ) {
              transitionWvState(WV_STATES.LOADED, 'progress 100%');
              webViewLoadState.current = 'loaded';
            }
          }}
          onLoadEnd={() => {
            // Only transition if still in LOADING state
            // (onLoadProgress might have already handled it)
            if (wvState.current === WV_STATES.LOADING) {
              transitionWvState(WV_STATES.LOADED, 'onLoadEnd');
              webViewLoadState.current = 'loaded';
            }
          }}
          onContentProcessDidTerminate={() =>
            handleWebViewTermination('iOS process terminated')
          }
          onRenderProcessGone={({ nativeEvent }) =>
            handleWebViewTermination(
              `Android renderer gone (didCrash=${nativeEvent.didCrash})`,
            )
          }
        />
      )}
    </WebViewContext.Provider>
  );
};

export const useWebView = () => React.useContext(WebViewContext);
