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

type TokenTransferJob = {
  creditTransactionId: string;
  creditGrantId: string;
  customerDid: string;
  amount: string;
  paymentCurrencyId: string;
  meterEventId: string;
  subscriptionId?: string;
};

type ValidationResult =
  | { valid: false }
  | {
      valid: true;
      creditTransaction: CreditTransaction;
      creditGrant: CreditGrant;
      customer: Customer;
      paymentCurrency: PaymentCurrency;
      meterEvent: MeterEvent;
      subscription?: Subscription;
    };

/**
 * Validate transfer job and fetch required data
 */
async function validateAndFetchData(job: TokenTransferJob): Promise<ValidationResult> {
  const creditTransaction = await CreditTransaction.findByPk(job.creditTransactionId);
  if (!creditTransaction) {
    logger.warn('CreditTransaction not found', { creditTransactionId: job.creditTransactionId });
    return { valid: false };
  }

  // Check if already transferred
  if (creditTransaction.transfer_status === 'completed') {
    logger.info('Token transfer already completed', { creditTransactionId: job.creditTransactionId });
    return { valid: false };
  }

  const creditGrant = await CreditGrant.findByPk(job.creditGrantId);
  if (!creditGrant) {
    logger.warn('CreditGrant not found', { creditGrantId: job.creditGrantId });
    return { valid: false };
  }

  const customer = await Customer.findByPkOrDid(job.customerDid);
  if (!customer) {
    logger.warn('Customer not found', { customerDid: job.customerDid });
    return { valid: false };
  }

  const paymentCurrency = await PaymentCurrency.findByPk(job.paymentCurrencyId);
  if (!paymentCurrency) {
    logger.warn('PaymentCurrency not found', { paymentCurrencyId: job.paymentCurrencyId });
    return { valid: false };
  }

  const meterEvent = await MeterEvent.findByPk(job.meterEventId);
  if (!meterEvent) {
    logger.warn('MeterEvent not found', { meterEventId: job.meterEventId });
    return { valid: false };
  }

  let subscription: Subscription | undefined;
  if (job.subscriptionId) {
    subscription = (await Subscription.findByPk(job.subscriptionId)) || undefined;
  }

  return {
    valid: true,
    creditTransaction,
    creditGrant,
    customer,
    paymentCurrency,
    meterEvent,
    subscription,
  };
}

/**
 * Handle token transfer job
 */
export async function handleTokenTransfer(job: TokenTransferJob): Promise<void> {
  logger.info('Starting token transfer job', {
    creditTransactionId: job.creditTransactionId,
    creditGrantId: job.creditGrantId,
    customerDid: job.customerDid,
    amount: job.amount,
  });

  const validation = await validateAndFetchData(job);
  if (!validation.valid) {
    logger.warn('Token transfer validation failed, skipping', { job });
    return;
  }

  const { creditTransaction, creditGrant, customer, paymentCurrency, meterEvent } = validation;

  // Get the latest DID for transfer (handles DID migration via on-chain state)
  const customerState = await getAccountState(paymentCurrency, customer.did);
  const customerDid = customerState?.address || customer.did;

  let txHash: string | null = null;
  // Record partial transfer details when chain balance is insufficient
  let transferResult: { expected: string; actual: string } | null = null;

  try {
    // Attempt token transfer
    txHash = await transferTokenFromCustomer({
      paymentCurrency,
      customerDid,
      amount: job.amount,
      data: {
        reason: 'credit_consumption',
        creditGrantId: creditGrant.id,
        meterEventId: meterEvent.id,
      },
    });

    logger.info('Token transfer completed successfully', {
      creditTransactionId: creditTransaction.id,
      txHash,
      from: customer.did,
      amount: job.amount,
      currency: paymentCurrency.symbol,
    });
  } catch (error: any) {
    const isInsufficientBalanceError =
      error?.message?.includes('does not have enough token') || error?.message?.includes('INSUFFICIENT_TOKEN_BALANCE');

    if (!isInsufficientBalanceError) {
      logger.error('Token transfer failed', {
        error,
        creditTransactionId: creditTransaction.id,
        customerDid: customer.did,
        amount: job.amount,
      });
      throw error;
    }

    // Handle insufficient balance: transfer whatever we can and mark as completed
    logger.warn('Insufficient chain balance detected', {
      creditTransactionId: creditTransaction.id,
      requestedAmount: job.amount,
      error,
    });

    // Get actual chain balance
    const chainBalance = await getCustomerTokenBalance(customerDid, paymentCurrency);
    const chainBalanceBN = new BN(chainBalance);
    const expectBalanceBN = new BN(job.amount);
    const chainDebt = expectBalanceBN.sub(chainBalanceBN);
    const transferAmount = BN.min(chainBalanceBN, expectBalanceBN);

    // Record transfer result for metadata (only when partial)
    if (chainDebt.gt(new BN(0))) {
      transferResult = {
        expected: job.amount,
        actual: transferAmount.toString(),
      };
    }

    if (chainBalanceBN.lt(expectBalanceBN)) {
      logger.error('CRITICAL: Chain balance is less than requested amount', {
        creditTransactionId: creditTransaction.id,
        chainBalance: chainBalance.toString(),
        requestedAmount: job.amount,
        chainDebt: chainDebt.toString(),
      });
    }

    // Transfer tokens if there is any balance
    if (chainBalanceBN.gt(new BN(0))) {
      txHash = await transferTokenFromCustomer({
        paymentCurrency,
        customerDid,
        amount: transferAmount.toString(),
        data: {
          reason: 'credit_consumption',
          creditGrantId: creditGrant.id,
          meterEventId: meterEvent.id,
        },
      });

      logger.warn('Partial token transfer completed - chain balance insufficient', {
        creditTransactionId: creditTransaction.id,
        customerDid,
        currencyId: paymentCurrency.id,
        actualBalance: chainBalanceBN.toString(),
        expectedAmount: expectBalanceBN.toString(),
        transferredAmount: transferAmount.toString(),
        chainDebt: chainDebt.toString(),
      });
    } else {
      logger.error('Zero chain balance - no tokens transferred', {
        creditTransactionId: creditTransaction.id,
        customerDid,
        currencyId: paymentCurrency.id,
        expectedAmount: expectBalanceBN.toString(),
      });
    }
  }

  // Update transaction with result
  await creditTransaction.update({
    transfer_status: 'completed',
    transfer_hash: txHash ?? undefined,
    ...(transferResult && {
      metadata: {
        ...(creditTransaction.metadata || {}),
        transfer_result: transferResult,
      },
    }),
  });
}

/**
 * Token transfer queue
 */
export const tokenTransferQueue = createQueue<TokenTransferJob>({
  name: 'token-transfer',
  onJob: handleTokenTransfer,
  options: {
    concurrency: 5,
    maxRetries: 3,
    retryDelay: 5000,
  },
});

tokenTransferQueue.on('finished', ({ id, job }) => {
  logger.debug('Token transfer job finished', { id, creditTransactionId: job.creditTransactionId });
});

tokenTransferQueue.on('failed', async ({ id, job, error }) => {
  logger.error('Token transfer job failed after all retries', { id, job, error: error.message });

  const creditTransaction = await CreditTransaction.findByPk(job.creditTransactionId);

  if (creditTransaction && creditTransaction.transfer_status !== 'completed') {
    await creditTransaction.update({
      transfer_status: 'failed',
    });
  }
});

tokenTransferQueue.on('retry', ({ id, job }) => {
  logger.info('Token transfer job retry scheduled', {
    id,
    creditTransactionId: job.creditTransactionId,
  });
});

/**
 * Add token transfer job to queue
 */
export async function addTokenTransferJob(job: TokenTransferJob): Promise<void> {
  const jobId = `token-transfer-${job.creditTransactionId}`;

  try {
    // Check if job already exists
    const existingJob = await tokenTransferQueue.get(jobId);
    if (existingJob) {
      logger.debug('Token transfer job already exists', { jobId, creditTransactionId: job.creditTransactionId });
      return;
    }

    // Add job to queue
    await tokenTransferQueue.push({ id: jobId, job });

    logger.info('Token transfer job added to queue', {
      jobId,
      creditTransactionId: job.creditTransactionId,
      customerDid: job.customerDid,
      amount: job.amount,
    });
  } catch (error: any) {
    logger.error('Failed to add token transfer job', {
      jobId,
      creditTransactionId: job.creditTransactionId,
      error,
    });
  }
}

/**
 * Start token transfer queue
 */
export async function startTokenTransferQueue(): Promise<void> {
  try {
    logger.info('Token transfer queue started');

    // Process any pending transfers on startup
    const pendingTransactions = await CreditTransaction.findAll({
      where: {
        transfer_status: 'pending',
      },
      limit: 100,
      order: [['created_at', 'DESC']],
    });

    logger.info('Found pending token transfers to process', { count: pendingTransactions.length });

    // Process in batches to add jobs
    await Promise.all(
      pendingTransactions.map(async (transaction) => {
        try {
          const customer = (await Customer.findByPk(transaction.customer_id))!;
          const creditGrant = (await CreditGrant.findByPk(transaction.credit_grant_id))!;

          await addTokenTransferJob({
            creditTransactionId: transaction.id,
            creditGrantId: transaction.credit_grant_id,
            customerDid: customer.did,
            amount: transaction.credit_amount,
            paymentCurrencyId: creditGrant.currency_id,
            meterEventId: transaction.source!,
            subscriptionId: transaction.subscription_id || undefined,
          });
        } catch (error: any) {
          logger.error('Failed to add pending transfer job', {
            transactionId: transaction.id,
            error: error.message,
          });
        }
      })
    );
  } catch (error: any) {
    logger.error('Failed to start token transfer queue', { error: error.message });
  }
}
