import {
  getSingleTxDetails,
  getSparkBitcoinPaymentRequest,
  getSparkLightningPaymentStatus,
  getSparkLightningSendRequest,
  getSparkPaymentStatus,
  getSparkTransactions,
  querySparkHodlLightningPayments,
  sparkPaymentType,
} from '.';
import {
  LightningSendRequestStatus,
  SparkCoopExitRequestStatus,
} from '@buildonspark/spark-sdk/types';
import {
  IS_BITCOIN_REQUEST_ID,
  IS_SPARK_ID,
  IS_SPARK_REQUEST_ID,
} from '../../constants';
import { getLocalStorageItem, setLocalStorageItem } from '../localStorage';
import {
  bulkUpdateSparkTransactions,
  deleteSparkTransaction,
  deleteUnpaidSparkLightningTransaction,
  getAllPendingSparkPayments,
  getAllSparkTransactions,
  getAllSparkContactInvoices,
  getAllUnpaidSparkLightningInvoices,
  getAllUnpaidHoldInvoicesFromTxs,
  getBulkPaymentGroupTransferIds,
} from './transactions';
import { transformTxToPaymentObject } from './transformTxToPayment';
import sha256Hash from '../hash';
import fetchBackend from '../../../db/handleBackend';
import i18next from 'i18next';

const RESTORE_STATE_KEY = 'spark_tx_restore_state';
const MAX_BATCH_SIZE = 400;
const DEFAULT_BATCH_SIZE = 5;
const INCREMENTAL_SAVE_THRESHOLD = 200;

/**
 * Get the current restore state for an account
 */
async function getRestoreState(accountId, numSavedTxs) {
  try {
    const stateJson = await getLocalStorageItem(
      `${RESTORE_STATE_KEY}_${accountId}`,
    );

    if (!stateJson) {
      // We assume if a user has over 400 saved txs, they are fully restored
      return {
        isFullyRestored: numSavedTxs > 400 ? true : false,
        lastProcessedOffset: 0,
        lastProcessedTxId: null,
        restoredTxCount: 0,
      };
    }
    return JSON.parse(stateJson);
  } catch (error) {
    console.error('Error getting restore state:', error);
    return {
      isFullyRestored: false,
      lastProcessedOffset: 0,
      lastProcessedTxId: null,
      restoredTxCount: 0,
    };
  }
}

/**
 * Update the restore state for an account
 */
async function updateRestoreState(accountId, state) {
  try {
    await setLocalStorageItem(
      `${RESTORE_STATE_KEY}_${accountId}`,
      JSON.stringify(state),
    );
  } catch (error) {
    console.error('Error updating restore state:', error);
  }
}

/**
 * Mark restoration as complete for an account
 */
async function markRestoreComplete(accountId) {
  await updateRestoreState(accountId, {
    isFullyRestored: true,
    lastProcessedOffset: 0,
    lastProcessedTxId: null,
    restoredTxCount: 0,
    completedAt: Date.now(),
  });
}

const restoreSparkTxState = async (
  BATCH_SIZE,
  identityPubKey,
  isSendingPayment,
  mnemonic,
  accountId,
  sendWebViewRequest,
  onProgressSave = null,
) => {
  const restoredTxs = [];

  try {
    const [savedTxs, pendingTxs] = await Promise.all([
      getAllSparkTransactions({ accountId: identityPubKey, idsOnly: true }),
      getAllPendingSparkPayments(accountId),
    ]);

    let savedIds = new Set(savedTxs);

    const bulkTransferIds = await getBulkPaymentGroupTransferIds(
      identityPubKey,
    );
    if (bulkTransferIds.size > 0) {
      savedIds = new Set([...savedIds, ...bulkTransferIds]);
    }

    const txsByType = {
      lightning: pendingTxs.filter(tx => tx.paymentType === 'lightning'),
      bitcoin: pendingTxs.filter(tx => tx.paymentType === 'bitcoin'),
    };
    const restoreState = await getRestoreState(accountId, savedIds.size);

    const isRestoring = !restoreState.isFullyRestored;
    let offset = isRestoring ? restoreState.lastProcessedOffset : 0;
    const localBatchSize = isRestoring ? MAX_BATCH_SIZE : BATCH_SIZE;

    console.log(
      `Restore mode: ${
        isRestoring ? 'ACTIVE' : 'NORMAL'
      }, batch size: ${localBatchSize}`,
    );

    const donationPubKey = process.env.BLITZ_SPARK_PUBLICKEY;

    const newTxsAtFront = [];
    if (isRestoring && offset > 0) {
      console.log('Checking for new transactions at the front...');
      try {
        const recentTxs = await getSparkTransactions(BATCH_SIZE, 0, mnemonic);
        const recentBatch = recentTxs.transfers || [];

        for (const tx of recentBatch) {
          if (savedIds.has(tx.id)) break;
          // Filter donations and active sends
          if (
            tx.transferDirection === 'OUTGOING' &&
            tx.receiverIdentityPublicKey === donationPubKey
          ) {
            continue;
          }
          if (tx.transferDirection === 'OUTGOING' && isSendingPayment) continue;

          const type = sparkPaymentType(tx);

          // Check against pending transactions
          if (type === 'bitcoin') {
            const duplicate = txsByType.bitcoin.find(item => {
              const details = JSON.parse(item.details);
              return (
                tx.transferDirection === details.direction &&
                tx.totalValue === details.amount &&
                details.time - new Date(tx.createdTime) < 1000 * 60 * 10
              );
            });
            if (duplicate) continue;
          } else if (type === 'lightning') {
            const duplicate = txsByType.lightning.find(item => {
              const details = JSON.parse(item.details);
              return (
                tx.transferDirection === details.direction &&
                details?.createdAt - new Date(tx.createdTime) < 1000 * 30
              );
            });
            if (duplicate) continue;
          }

          newTxsAtFront.push(tx);
        }

        if (newTxsAtFront.length > 0) {
          console.log(
            `Found ${newTxsAtFront.length} new transactions at the front`,
          );
          restoredTxs.push(...newTxsAtFront);
          // Add these new tx IDs to savedIds to avoid duplicates
          newTxsAtFront.forEach(tx => savedIds.add(tx.id));
        }
      } catch (error) {
        console.error('Error checking for new transactions:', error);
      }
    }

    let batchCounter = 0;
    let foundOverlap = false;

    while (true) {
      const txs = await getSparkTransactions(localBatchSize, offset, mnemonic);

      const batchTxs = txs.transfers || [];

      if (!batchTxs.length) {
        console.log('No more transactions found, ending restore.');
        await markRestoreComplete(accountId);
        break;
      }

      // Process batch and check for overlap simultaneously
      const newBatchTxs = [];
      for (const tx of batchTxs) {
        const type = sparkPaymentType(tx);

        const lnRequsestId = type === 'lightning' ? tx?.userRequest?.id : null;
        const paymentId = tx.id;
        // Check for overlap first (most likely to break early)
        if (
          savedIds.has(paymentId) ||
          (lnRequsestId && savedIds.has(lnRequsestId))
        ) {
          foundOverlap = true;
          console.log(
            'Found overlap with saved transactions, stopping restore.',
          );
          break;
        }

        // Filter out donation payments while processing
        if (
          tx.transferDirection === 'OUTGOING' &&
          tx.receiverIdentityPublicKey === donationPubKey
        ) {
          continue;
        }

        // This would cause a double transaction to be listed untill the pending items were clear
        if (tx.transferDirection === 'OUTGOING' && isSendingPayment) continue;

        if (type === 'bitcoin') {
          const response = txsByType.bitcoin.find(item => {
            const details = JSON.parse(item.details);
            return (
              tx.transferDirection === details.direction &&
              tx.totalValue === details.amount &&
              details.time - new Date(tx.createdTime) < 1000 * 60 * 10
            );
          });

          if (response) continue;
        } else if (type === 'lightning') {
          const response = txsByType.lightning.find(item => {
            const details = JSON.parse(item.details);
            return (
              tx.transferDirection === details.direction &&
              details?.createdAt - new Date(tx.createdTime) < 1000 * 30
            );
          });

          if (response) continue;
        }

        newBatchTxs.push(tx);
      }

      // Add filtered transactions to result
      restoredTxs.push(...newBatchTxs);
      batchCounter++;

      if (isRestoring && restoredTxs.length >= INCREMENTAL_SAVE_THRESHOLD) {
        console.log(`Incremental save: ${restoredTxs.length} transactions`);

        await updateRestoreState(accountId, {
          isFullyRestored: false,
          lastProcessedOffset: offset + localBatchSize,
          lastProcessedTxId: newBatchTxs[newBatchTxs.length - 1]?.id || null,
          restoredTxCount: restoreState.restoredTxCount + restoredTxs.length,
        });

        if (onProgressSave) {
          await onProgressSave(restoredTxs.slice());
        }

        restoredTxs.length = 0;
      }

      if (foundOverlap) {
        await markRestoreComplete(accountId);
        break;
      }

      offset += localBatchSize;
    }

    console.log(`Total restored transactions: ${restoredTxs.length}`);

    return {
      txs: restoredTxs,
      isRestoreComplete: !isRestoring || foundOverlap,
    };
  } catch (error) {
    console.error('Error in spark restore history state:', error);
    return { txs: [], isRestoreComplete: false };
  }
};

/**
 * Resolve a single transfer's details, preferring a pre-fetched batch cache so we
 * don't fire one getSingleTxDetails request per pending tx. Falls back to
 * getSingleTxDetails when the id isn't in the batch window (or no cache was built).
 */
async function resolveTransferDetails(id, mnemonic, transferCache) {
  const cached = transferCache?.get(id);
  if (cached) return cached;
  return await getSingleTxDetails(mnemonic, id);
}

// Helper function to split array into chunks
function chunkArray(array, chunkSize) {
  const chunks = [];
  for (let i = 0; i < array.length; i += chunkSize) {
    chunks.push(array.slice(i, i + chunkSize));
  }
  return chunks;
}

// Process a single chunk of transactions
async function processTransactionChunk(
  txChunk,
  sparkAddress,
  unpaidInvoices,
  identityPubKey,
  numberOfRestoredTxs,
  unpaidContactInvoices,
  mnemonic,
) {
  const chunkPaymentObjects = [];

  for (const tx of txChunk) {
    try {
      const paymentObject = await transformTxToPaymentObject(
        tx,
        sparkAddress,
        undefined,
        true,
        unpaidInvoices,
        identityPubKey,
        numberOfRestoredTxs,
        undefined,
        unpaidContactInvoices,
        mnemonic,
      );
      if (paymentObject) {
        chunkPaymentObjects.push(paymentObject);
      }
    } catch (err) {
      console.error('Error transforming tx:', tx.id, err);
    }
  }

  return chunkPaymentObjects;
}
let isRestoringState = false;
export async function fullRestoreSparkState({
  sparkAddress,
  batchSize = DEFAULT_BATCH_SIZE,
  chunkSize = 100,
  maxConcurrentChunks = 3, // Reduced for better responsiveness
  yieldInterval = 50, // Yield every N milliseconds
  onProgress = null, // Optional progress callback
  isSendingPayment,
  mnemonic,
  identityPubKey,
  sendWebViewRequest,
  isInitialRestore,
}) {
  try {
    if (isRestoringState) {
      console.log('already restoring state');
      return;
    }
    isRestoringState = true;
    console.log('running');

    const handleProgressSave = async txBatch => {
      if (!txBatch.length) return;

      const [unpaidInvoices, unpaidContactInvoices] = await Promise.all([
        getAllUnpaidSparkLightningInvoices(),
        getAllSparkContactInvoices(),
      ]);

      const paymentObjects = [];
      for (const tx of txBatch) {
        try {
          const paymentObject = await transformTxToPaymentObject(
            tx,
            sparkAddress,
            undefined,
            true,
            unpaidInvoices,
            identityPubKey,
            txBatch.length,
            undefined,
            unpaidContactInvoices,
            mnemonic,
          );
          if (paymentObject) {
            paymentObjects.push(paymentObject);
          }
        } catch (err) {
          console.error(
            'Error transforming tx during incremental save:',
            tx.id,
            err,
          );
        }
      }

      if (paymentObjects.length) {
        await bulkUpdateSparkTransactions(paymentObjects, 'incrementalRestore');
        console.log(
          `Incrementally saved ${paymentObjects.length} transactions`,
        );
      }
    };

    const restored = await restoreSparkTxState(
      batchSize,
      identityPubKey,
      isSendingPayment,
      mnemonic,
      identityPubKey,
      sendWebViewRequest,
      handleProgressSave,
    );
    if (!restored.txs.length) return;
    const [unpaidInvoices, unpaidContactInvoices] = await Promise.all([
      getAllUnpaidSparkLightningInvoices(),
      getAllSparkContactInvoices(),
    ]);
    const txChunks = chunkArray(restored.txs, chunkSize);

    console.log(
      `Processing ${restored.txs.length} transactions in ${txChunks.length} chunks`,
    );

    const allPaymentObjects = [];
    let processedChunks = 0;

    // Process chunks in smaller batches with yields
    for (let i = 0; i < txChunks.length; i += maxConcurrentChunks) {
      const batchChunks = txChunks.slice(i, i + maxConcurrentChunks);

      // Process this batch of chunks in parallel
      const chunkPromises = batchChunks.map(chunk =>
        processTransactionChunk(
          chunk,
          sparkAddress,
          unpaidInvoices,
          identityPubKey,
          restored.txs.length,
          unpaidContactInvoices,
          mnemonic,
        ),
      );

      try {
        const batchResults = await Promise.all(chunkPromises);
        allPaymentObjects.push(...batchResults.flat());
        processedChunks += batchChunks.length;

        // Call progress callback if provided
        if (onProgress) {
          onProgress({
            processed: processedChunks,
            total: txChunks.length,
            percentage: Math.round((processedChunks / txChunks.length) * 100),
          });
        }

        console.log(`Processed ${processedChunks}/${txChunks.length} chunks`);

        // Yield control back to main thread between batches
        if (i + maxConcurrentChunks < txChunks.length) {
          await new Promise(resolve => setTimeout(resolve, yieldInterval));
        }
      } catch (err) {
        console.error('Error processing chunk batch:', err);
      }
    }

    console.log(
      `Transformed ${allPaymentObjects.length}/${restored.txs.length} transactions`,
    );

    if (allPaymentObjects.length) {
      await bulkUpdateSparkTransactions(
        allPaymentObjects,
        `fullUpdate-waitBalance`,
      );
    }

    return allPaymentObjects.length;
  } catch (err) {
    console.log('full restore spark state error', err);
    return false;
  } finally {
    isRestoringState = false;
  }
}

function shouldRunOnThisTick(runcount, lastRunTimestamp) {
  if (runcount < 10) return true; // first 10 calls: let the 10s interval handle it naturally
  if (runcount > 22) return false; // after 21 calls, stop backoff and check every tick to avoid infinite backoff

  if (!lastRunTimestamp) return true;

  const backoffRun = runcount - 10; // 0-indexed backoff phase
  const backoffMs = Math.min(
    10_000 * Math.pow(2, backoffRun), // 10s, 20s, 40s, 80s...
    300_000, // cap at 5 minutes
  );

  return Date.now() - lastRunTimestamp >= backoffMs;
}

export async function checkFlashnetStablecoinStatusLogic(
  tx,
  contactsPrivateKey,
  publicKey,
) {
  try {
    const details =
      typeof tx.details === 'string' ? JSON.parse(tx.details) : tx.details;
    if (!details?.isFlashnetStablecoin || !details?.quoteId) return null;

    const runcount = details.runcount || 0;

    // Skip this tick if we haven't waited long enough
    if (!shouldRunOnThisTick(runcount, details.lastRunTimestamp)) return null;

    const statusResult = await fetchBackend(
      'checkFlashnetStablecoinStatus',
      {
        quoteId: details.quoteId,
        sourceSparkAddress: details.sourceSparkAddress,
        sparkTxHash: tx.sparkID,
      },
      contactsPrivateKey,
      publicKey,
    );

    if (!statusResult || statusResult.error)
      return {
        id: tx.sparkID,
        paymentStatus: details.runcount === 21 ? 'completed' : 'pending',
        paymentType: tx.paymentType,
        accountId: tx.accountId,
        details: {
          ...details,
          runcount: runcount + 1,
          lastRunTimestamp: Date.now(), // <-- persist when we last fetched
        },
      };

    const newStatus =
      statusResult.status === 'completed' || details.runcount === 21
        ? 'completed'
        : ['refunded', 'failed'].includes(statusResult.status)
        ? 'failed'
        : null;

    if (
      newStatus === 'failed' &&
      statusResult.refundTxHash &&
      statusResult.refundTxHash !== tx.sparkID
    ) {
      bulkUpdateSparkTransactions([
        {
          id: statusResult.refundTxHash,
          paymentStatus: 'completed',
          paymentType: 'unknown',
          accountId: tx.accountId,
          details: {
            description: i18next.t('constants.stablecoinRefundReceived'),
          },
        },
      ]).catch(err =>
        console.error('Error updating refund tx description:', err),
      );
    }

    if (!newStatus) return null;

    if (
      details.sarFundingTx &&
      newStatus === 'completed' &&
      statusResult.sparkTxHash &&
      statusResult.sparkTxHash !== tx.sparkID
    ) {
      bulkUpdateSparkTransactions([
        {
          id: statusResult.sparkTxHash,
          paymentStatus: 'completed',
          paymentType: 'unknown',
          accountId: tx.accountId,
          details: {
            description: i18next.t(
              'screens.inAccount.sendAndReplace.acceptingDescription',
            ),
            isSARIncoming: true,
          },
        },
      ]).catch(err =>
        console.error('Error updating SAR incoming tx description:', err),
      );
    }

    return {
      id: tx.sparkID,
      paymentStatus: newStatus,
      paymentType: tx.paymentType,
      accountId: tx.accountId,
      details: {
        ...details,
        runcount: runcount + 1,
        lastRunTimestamp: Date.now(), // <-- persist when we last fetched
      },
    };
  } catch {
    return null;
  }
}

let isUpdatingSparkTxStatus = false;
export const updateSparkTxStatus = async (
  mnemoninc,
  accountId,
  sendWebViewRequest,
  forceRefresh = false,
  contactsPrivateKey = null,
  publicKey = null,
) => {
  try {
    if (isUpdatingSparkTxStatus) {
      console.log('updateSparkTxStatus skipped: already running');
      return { shouldCheck: false };
    }
    isUpdatingSparkTxStatus = true;
    // Get all saved transactions
    console.log('running pending payments');
    const savedTxs = await getAllPendingSparkPayments(accountId);

    if (!savedTxs.length) return { updated: [], shouldCheck: true };
    const txsByType = {
      lightning: savedTxs.filter(tx => tx.paymentType === 'lightning'),
      bitcoin: savedTxs.filter(tx => tx.paymentType === 'bitcoin'),
      spark: savedTxs.filter(
        tx => tx.paymentType === 'spark' || tx.paymentType === 'unknown',
      ),
    };

    const [unpaidInvoices] = await Promise.all([
      txsByType.lightning.length
        ? getAllUnpaidSparkLightningInvoices()
        : Promise.resolve([]),
    ]);

    const unpaidInvoicesByAmount = new Map();
    unpaidInvoices.forEach(invoice => {
      const amount = invoice.amount;
      if (!unpaidInvoicesByAmount.has(amount)) {
        unpaidInvoicesByAmount.set(amount, []);
      }
      unpaidInvoicesByAmount.get(amount).push(invoice);
    });

    console.log('pending tx list', savedTxs);

    // For large pending sets, fetch the recent transfers once and look ids up
    // locally instead of one getSingleTxDetails request per pending tx. Only
    // 25 transfers since this runs every ~10s and each transfer is encrypted.
    let transferCache = null;
    if (savedTxs.length > 5) {
      try {
        const { transfers = [] } = await getSparkTransactions(
          25,
          undefined,
          mnemoninc,
        );
        transferCache = new Map(transfers.map(t => [t.id, t]));
      } catch (err) {
        console.error(
          'Error prefetching transfers for batch tx status update:',
          err,
        );
      }
    }

    // Process different transaction types in parallel
    const [lightningUpdates, bitcoinUpdates, sparkUpdates] = await Promise.all([
      processLightningTransactions(
        txsByType.lightning,
        unpaidInvoicesByAmount,
        mnemoninc,
        accountId,
        sendWebViewRequest,
        transferCache,
      ),
      processBitcoinTransactions(
        txsByType.bitcoin,
        mnemoninc,
        sendWebViewRequest,
        accountId,
        forceRefresh,
        transferCache,
      ),
      processSparkTransactions(
        txsByType.spark,
        mnemoninc,
        sendWebViewRequest,
        contactsPrivateKey,
        publicKey,
        transferCache,
      ),
    ]);

    const updatedTxs = [
      ...lightningUpdates,
      ...bitcoinUpdates,
      ...sparkUpdates.updatedTxs,
    ];

    if (!updatedTxs.length) return { updated: [], shouldCheck: false };

    await bulkUpdateSparkTransactions(
      updatedTxs,
      sparkUpdates.includesGift ? 'fullUpdate-waitBalance' : 'txStatusUpdate',
    );
    console.log(`Updated transactions:`, updatedTxs);
    return { updated: updatedTxs, shouldCheck: false };
  } catch (error) {
    console.error('Error in spark restore:', error);
    return { updated: [], shouldCheck: true };
  } finally {
    isUpdatingSparkTxStatus = false;
  }
};

async function processLightningTransactions(
  lightningTxs,
  unpaidInvoicesByAmount,
  mnemonic,
  accountId,
  sendWebViewRequest,
  transferCache,
) {
  const CONCURRENCY_LIMIT = 5;
  const updatedTxs = [];

  // Process in batches to avoid overwhelming the API
  for (let i = 0; i < lightningTxs.length; i += CONCURRENCY_LIMIT) {
    const batch = lightningTxs.slice(i, i + CONCURRENCY_LIMIT);

    const batchPromises = batch.map(tx =>
      processLightningTransaction(
        tx,
        unpaidInvoicesByAmount,
        mnemonic,
        sendWebViewRequest,
        transferCache,
      ).catch(err => {
        console.error('Error processing lightning tx:', tx.sparkID, err);
        return null;
      }),
    );

    const results = await Promise.all(batchPromises);
    const validResults = results.filter(Boolean);
    updatedTxs.push(...validResults);
  }

  let newTxs = [];

  for (const result of updatedTxs) {
    if (!result.lookThroughTxHistory) {
      newTxs.push(result);
      continue;
    }

    const findTxResponse = await resolveTransferDetails(
      result.id,
      mnemonic,
      transferCache,
    );

    if (!findTxResponse) {
      // If no transaction is found just call it completed
      const details = JSON.parse(result.txStateUpdate.details);
      newTxs.push({
        tempId: result.txStateUpdate.sparkID,
        useTempId: true,
        ...result.txStateUpdate,
        details,
        paymentStatus: 'completed',
      });
      continue;
    }

    const bitcoinTransfer = findTxResponse;

    const paymentStatus = getSparkPaymentStatus(bitcoinTransfer.status);
    const expiryDate = new Date(bitcoinTransfer.expiryTime);

    // remove any stale invoices or failed payments
    if (
      (paymentStatus === 'pending' && expiryDate < Date.now()) ||
      (bitcoinTransfer.transferDirection === 'OUTGOING' &&
        bitcoinTransfer.status === 'TRANSFER_STATUS_SENDER_KEY_TWEAK_PENDING')
    ) {
      await deleteSparkTransaction(result.id);
      continue;
    }

    const transformedObject = await transformTxToPaymentObject(
      bitcoinTransfer,
      undefined,
      undefined,
      false,
      [],
      accountId,
      1,
      false,
      [],
      mnemonic,
    );

    newTxs.push(transformedObject);
  }

  return newTxs;
}

async function processLightningTransaction(
  txStateUpdate,
  unpaidInvoicesByAmount,
  mnemonic,
  sendWebViewRequest,
  transferCache,
) {
  const details = JSON.parse(txStateUpdate.details);
  const possibleOptions = unpaidInvoicesByAmount.get(details.amount) || [];

  if (details.isHoldInvoice) {
    console.warn('Hold invoice do not check');
    return;
  }
  if (
    !IS_SPARK_REQUEST_ID.test(txStateUpdate.sparkID) &&
    !possibleOptions.length
  ) {
    console.log(txStateUpdate);
    // goes to be handled later by transform tx to payment
    return {
      id: txStateUpdate.sparkID,
      paymentStatus: '',
      paymentType: 'lightning',
      accountId: txStateUpdate.accountId,
      lookThroughTxHistory: true,
      txStateUpdate,
    };
  }

  if (!IS_SPARK_REQUEST_ID.test(txStateUpdate.sparkID)) {
    // Process invoice matching with retry logic
    const tx = await resolveTransferDetails(
      txStateUpdate.sparkID,
      mnemonic,
      transferCache,
    );

    if (!tx) return false;

    const userRequest = tx.userRequest;

    if (!userRequest?.id) return false;

    const savedInvoice = possibleOptions.find(
      item => item.sparkID === userRequest?.id,
    );

    const savedDetails = savedInvoice?.details
      ? JSON.parse(savedInvoice.details)
      : {};

    if (
      savedInvoice &&
      (!savedDetails.performSwaptoUSD ||
        (savedDetails.performSwaptoUSD && savedDetails.completedSwaptoUSD))
    ) {
      console.log(
        'Deleting lightning payment that was swapped to USD or a nomral LN payment that is now used',
      );
      deleteUnpaidSparkLightningTransaction(savedInvoice.sparkID);
    }

    if (savedDetails.performSwaptoUSD && !savedDetails.completedSwaptoUSD)
      return false;

    const isSendRequest = userRequest?.typename === 'LightningSendRequest';
    const invoice = userRequest
      ? isSendRequest
        ? userRequest?.encodedInvoice
        : userRequest.invoice?.encodedInvoice
      : '';
    const preimage = userRequest ? userRequest?.paymentPreimage || '' : '';

    return {
      useTempId: true,
      tempId: txStateUpdate.sparkID,
      id: tx.id ? tx.id : txStateUpdate.sparkID,
      paymentStatus: getSparkPaymentStatus(tx.status),
      paymentType: 'lightning',
      accountId: txStateUpdate.accountId,
      details: {
        ...savedDetails,
        description: savedInvoice?.description || '',
        address: invoice,
        preimage: preimage,
        shouldNavigate: savedInvoice?.shouldNavigate ?? 0,
        isLNURL: savedDetails?.isLNURL || false,
      },
    };
  }

  // Handle spark request IDs
  const sparkResponse =
    details.direction === 'INCOMING'
      ? await getSparkLightningPaymentStatus({
          lightningInvoiceId: txStateUpdate.sparkID,
          mnemonic,
        })
      : await getSparkLightningSendRequest(txStateUpdate.sparkID, mnemonic);

  const paymentStatus = getSparkPaymentStatus(sparkResponse.status);

  if (details.direction === 'OUTGOING' && paymentStatus === 'failed')
    return {
      ...txStateUpdate,
      useTempId: true,
      tempId: txStateUpdate.sparkID,
      id: sparkResponse.transfer.sparkId,
      details: {
        ...details,
      },
      paymentStatus: 'failed',
    };

  if (!sparkResponse?.transfer) return null;

  // const fee =
  //   sparkResponse.fee.originalValue /
  //   (sparkResponse.fee.originalUnit === 'MILLISATOSHI' ? 1000 : 1);

  const preimage = sparkResponse.paymentPreimage || '';

  if (!preimage) return null;

  return {
    useTempId: true,
    tempId: txStateUpdate.sparkID,
    id: sparkResponse.transfer.sparkId,
    paymentStatus:
      paymentStatus === 'completed' || preimage ? 'completed' : paymentStatus,
    paymentType: 'lightning',
    accountId: txStateUpdate.accountId,
    details: {
      ...details,
      // fee: Math.round(fee),
      // totalFee: Math.round(fee) + (details.supportFee || 0),
      preimage: preimage,
    },
  };
}

async function processBitcoinTransactions(
  bitcoinTxs,
  mnemonic,
  sendWebViewRequest,
  accountId,
  forceRefresh,
  transferCache,
) {
  const lastRun = await getLocalStorageItem('lastRunBitcoinTxUpdate');

  const now = Date.now();
  const cooldownPeriod = 1000 * 60; // 60 seconds
  let shouldBlockSendCheck = null;

  if (lastRun && now - JSON.parse(lastRun) < cooldownPeriod && !forceRefresh) {
    console.log('Blocking bitcoin transaction processing');
    shouldBlockSendCheck = true;
    return [];
  } else {
    console.log('Updating bitcoin transaction processing last run time');
    shouldBlockSendCheck = false;
    await setLocalStorageItem('lastRunBitcoinTxUpdate', JSON.stringify(now));
  }
  const updatedTxs = [];

  for (const txStateUpdate of bitcoinTxs) {
    const details = JSON.parse(txStateUpdate.details);

    if (
      details.direction === 'INCOMING' ||
      !IS_BITCOIN_REQUEST_ID.test(txStateUpdate.sparkID)
    ) {
      if (!IS_SPARK_ID.test(txStateUpdate.sparkID)) {
        const allPayments = await getAllSparkTransactions({ accountId });
        const foundPayment = allPayments.find(payment => {
          if (payment.paymentType === 'bitcoin') {
            const details = JSON.parse(payment.details);
            if (
              details.onChainTxid === txStateUpdate.sparkID &&
              payment.sparkID !== txStateUpdate.sparkID
            )
              return true;
          }
        });
        if (foundPayment) {
          const newDetails = JSON.parse(foundPayment.details);
          const oldDetails = JSON.parse(txStateUpdate.details);

          if (
            sha256Hash(JSON.stringify(foundPayment)) ===
            sha256Hash(JSON.stringify(txStateUpdate))
          )
            continue;

          updatedTxs.push({
            useTempId: true,
            tempId: txStateUpdate.sparkID,
            id: foundPayment.sparkID,
            paymentStatus: foundPayment.paymentStatus,
            paymentType: 'bitcoin',
            accountId: foundPayment.accountId,
            details: {
              ...newDetails,
              address: oldDetails.address || '',
              description: oldDetails.description || '',
            },
          });
        }
        continue;
      }

      const transfer = await resolveTransferDetails(
        txStateUpdate.sparkID,
        mnemonic,
        transferCache,
      );

      if (!transfer) continue;

      updatedTxs.push({
        id: txStateUpdate.sparkID,
        paymentStatus: getSparkPaymentStatus(transfer.status),
        paymentType: 'bitcoin',
        accountId: txStateUpdate.accountId,
      });
    } else {
      if (shouldBlockSendCheck) continue;
      const sparkResponse = await getSparkBitcoinPaymentRequest(
        txStateUpdate.sparkID,
        mnemonic,
      );

      if (!sparkResponse?.transfer) {
        if (
          sparkResponse?.coopExitTxid &&
          (!details.onChainTxid || !details.expiresAt)
        ) {
          updatedTxs.push({
            useTempId: true,
            tempId: txStateUpdate.sparkID,
            id: txStateUpdate.sparkID,
            paymentStatus: 'pending',
            paymentType: 'bitcoin',
            accountId: txStateUpdate.accountId,
            details: {
              ...details,
              onChainTxid: sparkResponse.coopExitTxid,
              expiresAt: sparkResponse.expiresAt || '',
            },
          });
        }

        if (
          sparkResponse.status === SparkCoopExitRequestStatus.EXPIRED ||
          sparkResponse.status === SparkCoopExitRequestStatus.FAILED
        ) {
          updatedTxs.push({
            id: txStateUpdate.sparkID,
            paymentStatus: 'failed',
            paymentType: 'bitcoin',
            accountId: txStateUpdate.accountId,
            details,
          });
        }
        continue;
      }

      updatedTxs.push({
        useTempId: true,
        tempId: txStateUpdate.sparkID,
        id: sparkResponse.transfer.sparkId,
        paymentStatus: getSparkPaymentStatus(sparkResponse.status),
        paymentType: 'bitcoin',
        accountId: txStateUpdate.accountId,
        details: {
          ...details,
          onChainTxid: sparkResponse.coopExitTxid,
        },
      });
    }
  }

  return updatedTxs;
}

async function processSparkTransactions(
  sparkTxs,
  mnemonic,
  sendWebViewRequest,
  contactsPrivateKey = null,
  publicKey = null,
  transferCache,
) {
  let includesGift = false;
  let updatedTxs = [];
  for (const txStateUpdate of sparkTxs) {
    const details = JSON.parse(txStateUpdate.details);

    // Stablecoin sends via Flashnet orchestration — delegate to status checker
    if (details.isFlashnetStablecoin) {
      if (contactsPrivateKey && publicKey) {
        const update = await checkFlashnetStablecoinStatusLogic(
          txStateUpdate,
          contactsPrivateKey,
          publicKey,
        );
        if (update) updatedTxs.push(update);
      }
      continue;
    }

    if (IS_SPARK_ID.test(txStateUpdate.sparkID)) {
      // This means the placeholder tx is created and we should defer this action to the debouceHandlIncomePayment function
      if (txStateUpdate.paymentType === 'unknown' && !details.amount) continue;
      const findTxResponse = await resolveTransferDetails(
        txStateUpdate.sparkID,
        mnemonic,
        transferCache,
      );

      if (!findTxResponse) continue;

      if (details.isGift) {
        includesGift = true;
      }

      updatedTxs.push({
        id: txStateUpdate.sparkID,
        paymentStatus: getSparkPaymentStatus(findTxResponse.status),
        paymentType: 'spark',
        accountId: txStateUpdate.accountId,
      });
    } else {
      if (details.isGift) {
        // dont process lrc20 pending gift payments, will be handled by getLRC20Transactions function that loops every 10s
        continue;
      }
      updatedTxs.push({
        id: txStateUpdate.sparkID,
        paymentStatus: 'completed',
        paymentType: 'spark',
        accountId: txStateUpdate.accountId,
      });
    }
  }

  return { updatedTxs, includesGift };
}

export const checkHodlInvoicePaymentStatuses = async (
  mnemonic,
  identityPubKey,
) => {
  try {
    const [unpaidInvoices, pendingHoldInvoices] = await Promise.all([
      getAllUnpaidSparkLightningInvoices(),
      getAllUnpaidHoldInvoicesFromTxs(),
    ]);

    if (!unpaidInvoices?.length && !pendingHoldInvoices?.length) return;

    const holdInvoices = unpaidInvoices
      .map(inv => ({
        ...inv,
        details:
          typeof inv.details === 'string'
            ? JSON.parse(inv.details)
            : inv.details,
      }))
      .filter(inv => inv.details?.isHoldInvoice === true);

    if (!holdInvoices.length && !pendingHoldInvoices.length) return;

    const paymentHashes = [...holdInvoices, ...pendingHoldInvoices]
      .map(inv => inv.details.paymentHash)
      .filter(Boolean);

    const queryResult = await querySparkHodlLightningPayments({
      paymentHashes,
      mnemonic,
    });
    console.log(queryResult, 'query result');
    if (!queryResult.didWork || !queryResult?.paidPreimages?.length) return;

    const txsToAdd = [];
    const txsToUpdate = [];
    const idsToDelete = [];

    const unpaidByHash = new Map(
      holdInvoices
        .filter(inv => inv.details?.paymentHash)
        .map(inv => [inv.details.paymentHash, inv]),
    );

    const pendingByHash = new Map(
      pendingHoldInvoices
        .filter(inv => inv.details?.paymentHash)
        .map(inv => [inv.details.paymentHash, inv]),
    );

    for (const preimageRequest of queryResult.paidPreimages) {
      console.log(preimageRequest, 'reimagme requset in array');
      const hashHex =
        typeof preimageRequest.paymentHash === 'string'
          ? preimageRequest.paymentHash
          : Buffer.from(preimageRequest.paymentHash).toString('hex');

      // Check both lists for a match
      const matchFromUnpaid = unpaidByHash.get(hashHex);
      const matchFromPending = pendingByHash.get(hashHex);

      // Handle unpaid invoice matches (existing logic)
      if (matchFromUnpaid) {
        if (!preimageRequest.transferId) continue;

        if (preimageRequest.status === 0) {
          txsToAdd.push({
            id: preimageRequest.transferId,
            paymentStatus: 'pending',
            paymentType: 'lightning',
            accountId: identityPubKey,
            details: {
              amount: matchFromUnpaid.amount || preimageRequest.satValue,
              fee: 0,
              time: preimageRequest.createdTime
                ? new Date(preimageRequest.createdTime).getTime()
                : Date.now(),
              direction: 'INCOMING',
              description: matchFromUnpaid.description,
              isHoldInvoice: true,
              encryptedPreimage: matchFromUnpaid.details.encryptedPreimage,
              paymentHash: matchFromUnpaid.details.paymentHash,
              dateAddedToDb: Date.now(),
            },
          });
        }

        if (preimageRequest.status === 0 || preimageRequest.status === 2) {
          idsToDelete.push(matchFromUnpaid.sparkID);
        }
      }

      // Handle pending hold invoice matches (new logic)
      if (matchFromPending) {
        if (preimageRequest.status === 1) {
          txsToUpdate.push({
            id: matchFromPending.sparkID,
            paymentStatus: 'completed',
            paymentType: matchFromPending.paymentType,
            accountId: identityPubKey,
            details: {
              ...matchFromPending.details,
            },
          });
        } else if (preimageRequest.status === 2) {
          txsToUpdate.push({
            id: matchFromPending.sparkID,
            paymentStatus: 'failed',
            paymentType: matchFromPending.paymentType,
            accountId: identityPubKey,
            details: {
              ...matchFromPending.details,
            },
          });
        }
      }
    }

    const allTxChanges = [...txsToAdd, ...txsToUpdate];

    if (allTxChanges.length > 0) {
      await bulkUpdateSparkTransactions(allTxChanges);
    }

    for (const sparkID of idsToDelete) {
      await deleteUnpaidSparkLightningTransaction(sparkID);
    }
  } catch (err) {
    console.error('Error checking hold invoice payment statuses:', err);
  }
};
