/**
 * Credit Token Balance Reconciliation Queue
 *
 * Verifies that customer's on-chain token balance matches database records.
 * Triggered after mint (grant) and burn (expire) operations.
 */

import { BN } from '@ocap/util';
import logger from '../libs/logger';
import createQueue from '../libs/queue';
import { getAccountState, getCustomerTokenBalance } from '../integrations/arcblock/token';
import { CreditGrant, CreditTransaction, Customer, PaymentCurrency } from '../store/models';
import { events } from '../libs/event';

type ReconciliationJob = {
  customerId: string;
  currencyId: string;
  creditGrantId: string;
  trigger: 'mint' | 'burn';
};

type BalanceInfo = {
  // Sum of remaining_amount in all granted grants
  dbRemainingAmount: string;
  // Sum of credit_amount in pending transfers (consumed but not yet transferred on-chain)
  pendingTransferAmount: string;
  // Expected on-chain balance = dbRemainingAmount + pendingTransferAmount
  expectedChainBalance: string;
};

/**
 * Calculate customer's balance info from database
 *
 * The expected chain balance should be:
 * - DB remaining_amount (what's left in grants that have been minted)
 * - Plus pending transfer amount (consumed in DB but not yet transferred on-chain)
 *
 * Only counts grants with chain_status === 'mint_completed' since those have on-chain tokens.
 */
async function getBalanceInfo(customerId: string, currencyId: string): Promise<BalanceInfo> {
  // Get all granted credit grants that have been minted on-chain
  const grants = await CreditGrant.findAll({
    where: {
      customer_id: customerId,
      currency_id: currencyId,
      status: 'granted',
      chain_status: 'mint_completed', // Only count grants with on-chain tokens
    },
  });

  const dbRemainingAmount = grants
    .reduce((sum, grant) => sum.add(new BN(grant.remaining_amount)), new BN(0))
    .toString();

  // Get pending transfers (consumed but token not yet transferred on-chain)
  const pendingTransactions = await CreditTransaction.findAll({
    where: {
      customer_id: customerId,
      transfer_status: 'pending',
    },
    include: [
      {
        model: CreditGrant,
        as: 'creditGrant',
        where: { currency_id: currencyId },
        required: true,
        attributes: [],
      },
    ],
  });

  const pendingTransferAmount = pendingTransactions
    .reduce((sum, tx) => sum.add(new BN(tx.credit_amount)), new BN(0))
    .toString();

  // Expected chain balance = remaining in grants + pending transfers
  // Because: consumed credit is deducted from grant but token is still in user's wallet
  const expectedChainBalance = new BN(dbRemainingAmount).add(new BN(pendingTransferAmount)).toString();

  return {
    dbRemainingAmount,
    pendingTransferAmount,
    expectedChainBalance,
  };
}

/**
 * Handle reconciliation job
 */
async function handleReconciliation(job: ReconciliationJob) {
  const { customerId, currencyId, creditGrantId, trigger } = job;

  logger.info('Starting credit balance reconciliation', {
    customerId,
    currencyId,
    creditGrantId,
    trigger,
  });

  try {
    const currency = await PaymentCurrency.findByPk(currencyId);
    if (!currency || !currency.isOnChainCredit()) {
      logger.warn('Currency not found or not on-chain credit', { currencyId });
      return;
    }

    const customer = await Customer.findByPk(customerId);
    if (!customer?.did) {
      logger.warn('Customer not found for reconciliation', { customerId });
      return;
    }
    const customerState = await getAccountState(currency, customer.did);
    const customerDid = customerState?.address || customer.did;

    // Get balance info (considering pending transfers)
    const balanceInfo = await getBalanceInfo(customerId, currencyId);
    const chainBalance = await getCustomerTokenBalance(customerDid, currency);

    const difference = new BN(chainBalance).sub(new BN(balanceInfo.expectedChainBalance));
    const isMatched = difference.eq(new BN(0));

    if (isMatched) {
      logger.info('Credit balance reconciliation passed', {
        customerId,
        currencyId,
        creditGrantId,
        trigger,
        dbRemainingAmount: balanceInfo.dbRemainingAmount,
        pendingTransferAmount: balanceInfo.pendingTransferAmount,
        expectedChainBalance: balanceInfo.expectedChainBalance,
        actualChainBalance: chainBalance,
      });
    } else {
      logger.error('Credit balance mismatch detected', {
        customerId,
        customerDid,
        currencyId,
        creditGrantId,
        trigger,
        dbRemainingAmount: balanceInfo.dbRemainingAmount,
        pendingTransferAmount: balanceInfo.pendingTransferAmount,
        expectedChainBalance: balanceInfo.expectedChainBalance,
        actualChainBalance: chainBalance,
        difference: difference.toString(),
      });

      // Emit event for monitoring/alerting
      events.emit('customer.credit.reconciliation.mismatch', {
        customerId,
        customerDid,
        currencyId,
        creditGrantId,
        trigger,
        dbRemainingAmount: balanceInfo.dbRemainingAmount,
        pendingTransferAmount: balanceInfo.pendingTransferAmount,
        expectedChainBalance: balanceInfo.expectedChainBalance,
        actualChainBalance: chainBalance,
        difference: difference.toString(),
      });
    }
  } catch (error: any) {
    logger.error('Credit balance reconciliation failed', {
      customerId,
      currencyId,
      creditGrantId,
      trigger,
      error: error.message,
    });
  }
}

/**
 * Reconciliation queue
 */
export const reconciliationQueue = createQueue<ReconciliationJob>({
  name: 'credit-reconciliation',
  onJob: handleReconciliation,
  options: {
    concurrency: 3,
    maxRetries: 2,
    retryDelay: 5000,
    enableScheduledJob: true,
  },
});

reconciliationQueue.on('finished', ({ id, job }) => {
  logger.debug('Reconciliation job completed', { id, customerId: job.customerId });
});

reconciliationQueue.on('failed', ({ id, job, error }) => {
  logger.error('Reconciliation job failed', { id, customerId: job.customerId, error: error.message });
});

/**
 * Add reconciliation job to queue
 * @param delay - delay in seconds (default: 5)
 */
async function addReconciliationJob(
  customerId: string,
  currencyId: string,
  creditGrantId: string,
  trigger: 'mint' | 'burn',
  delay: number = 5
): Promise<void> {
  const jobId = `reconcile-${customerId}-${currencyId}`;

  // Skip if already queued
  const existingJob = await reconciliationQueue.get(jobId);
  if (existingJob) {
    logger.debug('Reconciliation job already queued', { jobId });
    return;
  }

  await reconciliationQueue.push({
    id: jobId,
    job: { customerId, currencyId, creditGrantId, trigger },
    delay,
  });

  logger.info('Reconciliation job added', { jobId, trigger, delay });
}

/**
 * Start reconciliation queue and register event listeners
 */
// eslint-disable-next-line require-await
export async function startReconciliationQueue() {
  logger.info('Starting credit reconciliation queue');

  // Trigger reconciliation after successful mint
  events.on('customer.credit_grant.minted', async (creditGrant: CreditGrant) => {
    try {
      await addReconciliationJob(creditGrant.customer_id, creditGrant.currency_id, creditGrant.id, 'mint');
    } catch (error: any) {
      logger.error('Failed to add reconciliation job after mint', {
        creditGrantId: creditGrant.id,
        error: error.message,
      });
    }
  });

  // Trigger reconciliation after successful burn
  events.on('customer.credit_grant.burned', async (creditGrant: CreditGrant) => {
    try {
      await addReconciliationJob(creditGrant.customer_id, creditGrant.currency_id, creditGrant.id, 'burn');
    } catch (error: any) {
      logger.error('Failed to add reconciliation job after burn', {
        creditGrantId: creditGrant.id,
        error: error.message,
      });
    }
  });

  logger.info('Credit reconciliation queue started');
}
