import { BN, fromUnitToToken } from '@ocap/util';
import { isRefundReasonSupportedByStripe } from '../libs/refund';
import { checkRemainingStake, getSubscriptionStakeAddress } from '../libs/subscription';
import { sendErc20ToUser } from '../integrations/ethereum/token';
import { wallet } from '../libs/auth';
import CustomError, { NonRetryableError } from '../libs/error';
import { events } from '../libs/event';
import logger from '../libs/logger';
import { getGasPayerExtra, isBalanceSufficientForRefund } from '../libs/payment';
import createQueue from '../libs/queue';
import { MAX_RETRY_COUNT, getNextRetry } from '../libs/util';
import { Customer } from '../store/models/customer';
import { PaymentCurrency } from '../store/models/payment-currency';
import { PaymentIntent } from '../store/models/payment-intent';
import { PaymentMethod } from '../store/models/payment-method';
import { Refund } from '../store/models/refund';
import { Subscription } from '../store/models/subscription';
import { CreditGrant } from '../store/models/credit-grant';
import { Invoice } from '../store/models/invoice';
import { burnToken, getAccountState } from '../integrations/arcblock/token';
import type { EVMChainType, PaymentError } from '../store/models/types';
import { EVM_CHAIN_TYPES } from '../libs/constants';

type RefundJob = {
  refundId: string;
  retryOnError?: boolean;
};

type Updates = {
  retry: {
    refund: Partial<Refund>;
  };
  terminate: {
    refund: Partial<Refund>;
  };
};

const markRefundNonRetryable = async (refund: Refund, code: string, message: string, paymentMethod?: PaymentMethod) => {
  await refund.update({
    status: 'requires_action',
    last_attempt_error: {
      type: 'invalid_request_error',
      code,
      message,
      payment_method_id: paymentMethod?.id,
      payment_method_type: paymentMethod?.type,
    },
    attempt_count: refund.attempt_count + 1,
    attempted: true,
  });
};

export const handleRefundFailed = (refund: Refund, error: PaymentError) => {
  const attemptCount = refund.attempt_count + 1;
  const updates: Updates = {
    retry: {
      refund: {
        status: 'pending',
        last_attempt_error: error,
        attempt_count: attemptCount,
        attempted: true,
        next_attempt: getNextRetry(attemptCount),
      },
    },
    terminate: {
      refund: {
        status: 'requires_action',
        last_attempt_error: error,
        attempt_count: attemptCount,
        attempted: true,
      },
    },
  };

  // check max retry
  if (refund.attempt_count > MAX_RETRY_COUNT) {
    return updates.terminate;
  }

  return updates.retry;
};

export const handleRefund = async (job: RefundJob) => {
  logger.info('handle refund', job);

  const refund = await Refund.findByPk(job.refundId);
  if (!refund) {
    logger.warn(`refund not found: ${job.refundId}`);
    return;
  }

  if (['pending'].includes(refund.status) === false) {
    logger.warn(`refund status not expected: ${refund.status}`);
    return;
  }

  // check max retry before doing any hard work
  if (refund.attempt_count > MAX_RETRY_COUNT) {
    logger.info(`refund transfer aborted since max retry exceeded: ${refund.id}`);
    const updates = handleRefundFailed(refund, {
      type: 'card_error',
      code: 'max_retry_exceeded',
      message: 'max_retry_exceeded',
    });
    await refund.update(updates.refund);
    return;
  }

  const paymentCurrency = await PaymentCurrency.findByPk(refund.currency_id);
  if (!paymentCurrency) {
    logger.warn(`PaymentCurrency not found: ${refund.currency_id}`);
    await markRefundNonRetryable(refund, 'CURRENCY_NOT_FOUND', 'Payment currency not found');
    return;
  }
  const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
  if (!paymentMethod) {
    logger.warn(`PaymentMethod not found: ${paymentCurrency.payment_method_id}`);
    await markRefundNonRetryable(refund, 'PAYMENT_METHOD_NOT_FOUND', 'Payment method not found');
    return;
  }

  const customer = await Customer.findByPk(refund.customer_id);
  if (!customer) {
    logger.warn(`Customer not found: ${refund.customer_id}`);
    await markRefundNonRetryable(refund, 'CUSTOMER_NOT_FOUND', 'Customer not found', paymentMethod);
    return;
  }

  if (refund?.type === 'stake_return') {
    await handleStakeReturnJob(job, refund, paymentCurrency, paymentMethod, customer);
  } else if (paymentMethod) {
    await handleRefundJob(job, refund, paymentCurrency, paymentMethod, customer);
  }
};

/**
 * Finalize refund after transfer succeeds
 * For credit purchase refunds: set processing -> burn credit -> set succeeded/failed
 * For other types: directly set succeeded
 */
async function finalizeRefundAfterTransfer(refund: Refund, paymentDetails: any): Promise<void> {
  try {
    // Try to burn credit tokens if this refund is for a credit purchase
    const totalBurned = await handleOnchainCreditRefund(refund);

    // All succeeded, update status
    await refund.update({
      status: 'succeeded',
      last_attempt_error: null,
      payment_details: paymentDetails,
    });
    logger.info('Refund succeeded', {
      id: refund.id,
      totalBurned,
      hasCreditBurn: !!totalBurned && totalBurned !== '0',
    });
  } catch (error: any) {
    // Burn failed, set processing status and record error
    await refund.update({
      status: 'processing',
      payment_details: paymentDetails,
      last_attempt_error: {
        type: 'credit_burn_error',
        code: 'BURN_FAILED',
        message: error.message || 'Failed to burn credit tokens',
      },
    });
    logger.error('Credit burn failed, refund set to processing', {
      id: refund.id,
      error: error.message,
    });
  }
}

const handleRefundJob = async (
  job: RefundJob,
  refund: Refund,
  paymentCurrency: PaymentCurrency,
  paymentMethod: PaymentMethod,
  customer: Customer
) => {
  const refundSupport = ['arcblock', 'ethereum', 'stripe', 'base'].includes(paymentMethod.type);
  if (refundSupport === false) {
    logger.warn(`PaymentMethod does not support auto charge: ${paymentCurrency.payment_method_id}`);
    await markRefundNonRetryable(
      refund,
      'PAYMENT_METHOD_NOT_SUPPORTED',
      'Payment method does not support refund',
      paymentMethod
    );
    return;
  }

  // try refund transfer and reschedule on error
  logger.info('Refund transfer attempt', { id: refund.id, attempt: refund.attempt_count });
  try {
    if (!refund.payment_intent_id) {
      throw new NonRetryableError('PAYMENT_INTENT_NOT_FOUND', 'payment_intent_id is missing for refund');
    }
    const paymentIntent = await PaymentIntent.findByPk(refund.payment_intent_id);
    if (!paymentIntent) {
      throw new NonRetryableError('PAYMENT_INTENT_NOT_FOUND', 'PaymentIntent not found');
    }
    let result;
    if (paymentMethod.type === 'arcblock') {
      const client = paymentMethod.getOcapClient();
      // check balance before transfer with transaction
      result = await isBalanceSufficientForRefund({ paymentMethod, paymentCurrency, amount: refund.amount });
      if (result.sufficient === false) {
        logger.error('refund transfer aborted on preCheck', { id: refund.id, result });
        throw new CustomError(result.reason, 'app balance not sufficient for this refund');
      }

      const payer = paymentIntent?.payment_details?.arcblock?.payer;
      // do the transfer
      const signed = await client.signTransferV2Tx({
        tx: {
          itx: {
            to: payer || customer.did,
            value: '0',
            assets: [],
            tokens: [{ address: paymentCurrency.contract, value: refund.amount }],
            data: {
              typeUrl: 'json',
              // @ts-ignore
              value: {
                appId: wallet.address,
                reason: refund.reason,
                refundId: refund.id,
              },
            },
          },
        },
        wallet,
      });
      // @ts-ignore
      const { buffer } = await client.encodeTransferV2Tx({ tx: signed });
      // @ts-ignore
      const txHash = await client.sendTransferV2Tx({ tx: signed, wallet }, await getGasPayerExtra(buffer));

      logger.info('refund transfer done', { id: refund.id, txHash });
      await finalizeRefundAfterTransfer(refund, {
        arcblock: {
          tx_hash: txHash,
          payer: wallet.address,
          type: 'transfer',
        },
      });
    }

    if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
      const paymentType = paymentMethod.type;
      if (!paymentCurrency.contract) {
        throw new Error(`Refund not supported for ${paymentType} payment currencies without contract`);
      }

      // check balance before transfer with transaction
      result = await isBalanceSufficientForRefund({ paymentMethod, paymentCurrency, amount: refund.amount });
      if (result.sufficient === false) {
        logger.error('refund transfer aborted on preCheck', { id: refund.id, result });
        throw new CustomError(result.reason, 'app balance not sufficient for this refund');
      }

      // do the capture
      const client = paymentMethod.getEvmClient();
      const payer = paymentIntent!.payment_details?.[paymentType as EVMChainType]?.payer as string;
      const receipt = await sendErc20ToUser(client, paymentCurrency.contract, payer, refund.amount);
      logger.info('refund transfer done', { id: refund.id, txHash: receipt.hash });

      await finalizeRefundAfterTransfer(refund, {
        [paymentType]: {
          tx_hash: receipt.hash,
          payer: wallet.address,
          block_height: receipt.blockNumber.toString(),
          gas_used: receipt.gasUsed.toString(),
          gas_price: receipt.gasPrice.toString(),
          type: 'transfer',
        },
      });
    }
    if (paymentMethod.type === 'stripe') {
      if (!refund.payment_intent_id) {
        throw new Error('payment intent id not found');
      }
      const stripePaymentIntentId = paymentIntent?.payment_details?.stripe?.payment_intent_id;
      if (!stripePaymentIntentId) {
        throw new Error('paymentIntent should have stripe payment intent id');
      }
      const client = paymentMethod.getStripeClient();
      const stripePaymentIntent = await client.paymentIntents.retrieve(stripePaymentIntentId);
      const chargeId = stripePaymentIntent.latest_charge;
      if (!chargeId) {
        throw new Error('charge id not found');
      }
      if (!refund.reason) {
        throw new Error('refund reason not found');
      }

      const stripeRefund = await client.refunds.create({
        // @ts-ignore
        charge: chargeId,
        amount: refund.amount,
        ...(isRefundReasonSupportedByStripe(refund.reason) ? { reason: refund.reason } : {}),
      });
      if (stripeRefund.status === 'succeeded') {
        await finalizeRefundAfterTransfer(refund, {
          stripe: {
            payment_intent_id: stripePaymentIntentId,
            refund_id: stripeRefund.id,
          },
        });
      }
    }
  } catch (err) {
    logger.error('refund transfer failed', { error: err, id: refund.id });

    if (err instanceof NonRetryableError || err?.nonRetryable === true) {
      await markRefundNonRetryable(refund, err.code, err.message, paymentMethod);
      logger.error('refund transfer aborted: non-retryable error', { id: refund.id, code: err.code });
      return;
    }

    const error: PaymentError = {
      type: 'card_error',
      code: err.code,
      message: err.message,
      payment_method_id: paymentMethod.id,
      payment_method_type: paymentMethod.type,
    };

    const updates = await handleRefundFailed(refund, error);
    await refund.update(updates.refund);

    // reschedule next attempt
    const retryAt = updates.refund.next_attempt;
    if (retryAt) {
      refundQueue.push({
        id: refund.id,
        job: { refundId: refund.id, retryOnError: job.retryOnError },
        runAt: retryAt,
      });
      logger.error('refund transfer retry scheduled', { id: refund.id, retryAt });
    } else {
      logger.info('refund job deleted since no retry', { id: refund.id });
      refundQueue.delete(refund.id);
    }
  }
};

const handleStakeReturnJob = async (
  job: RefundJob,
  refund: Refund,
  paymentCurrency: PaymentCurrency,
  paymentMethod: PaymentMethod,
  customer: Customer
) => {
  // try stake return and reschedule on error
  logger.info('Stake return attempt', { id: refund.id, attempt: refund.attempt_count });
  try {
    if (paymentMethod.type === 'arcblock') {
      const arcblockDetail = refund.payment_details?.arcblock;
      if (!arcblockDetail) {
        throw new Error('arcblockDetail info not found');
      }

      if (refund.amount === '0') {
        logger.warn('stake return aborted because amount is 0', { id: refund.id });
        await refund.update({
          status: 'failed',
          last_attempt_error: {
            type: 'card_error',
            code: '400',
            message: 'stake return amount is 0',
            payment_method_id: paymentMethod.id,
            payment_method_type: paymentMethod.type,
          },
        });
        return;
      }
      const client = paymentMethod.getOcapClient();
      const subscription = await Subscription.findByPk(refund.subscription_id);
      const address =
        arcblockDetail?.staking?.address ||
        (await getSubscriptionStakeAddress(subscription!, customer.did, paymentMethod));
      const stakeEnough = await checkRemainingStake(paymentMethod, paymentCurrency, address, refund.amount);
      if (!stakeEnough.enough) {
        logger.warn('Stake return aborted because stake is not enough ', {
          subscription: refund.subscription_id,
          address,
          staked: stakeEnough.staked,
          revoked: stakeEnough.revoked,
        });
        await refund.update({
          status: 'failed',
          last_attempt_error: {
            type: 'card_error',
            code: '400',
            message: 'stake is not enough',
            payment_method_id: paymentMethod.id,
            payment_method_type: paymentMethod.type,
          },
        });
        return;
      }
      const signed = await client.signReturnStakeTx({
        tx: {
          itx: {
            address,
            outputs: [
              {
                owner: arcblockDetail?.receiver,
                tokens: [{ address: paymentCurrency.contract, value: refund.amount }],
              },
            ],
            message: refund.description || 'stake_return_on_subscription_cancel',
            data: {
              typeUrl: 'json',
              // @ts-ignore
              value: {
                appId: wallet.address,
                reason: refund?.metadata?.reason || 'subscription_cancel',
                subscriptionId: refund.subscription_id,
              },
            },
          },
        },
        wallet,
      });
      // @ts-ignore
      const { buffer } = await client.encodeReturnStakeTx({ tx: signed });
      // @ts-ignore
      const txHash = await client.sendReturnStakeTx({ tx: signed, wallet }, await getGasPayerExtra(buffer));
      logger.info('stake return done', { id: refund.id, txHash });
      await refund.update({
        status: 'succeeded',
        last_attempt_error: null,
        payment_details: {
          arcblock: {
            tx_hash: txHash,
            payer: wallet.address,
            type: 'stake_return',
            receiver: arcblockDetail?.receiver,
          },
        },
      });
      logger.info('Stake return refund status updated to succeeded', { id: refund.id, txHash });
    }
  } catch (err: any) {
    logger.error('stake return failed', { error: err, id: refund.id });

    const error: PaymentError = {
      type: 'card_error',
      code: err.code,
      message: err.message,
      payment_method_id: paymentMethod.id,
      payment_method_type: paymentMethod.type,
    };

    const updates = await handleRefundFailed(refund, error);
    await refund.update(updates.refund);

    // reschedule next attempt
    const retryAt = updates.refund.next_attempt;
    if (retryAt) {
      refundQueue.push({
        id: refund.id,
        job: { refundId: refund.id, retryOnError: job.retryOnError },
        runAt: retryAt,
      });
      logger.error('stake return retry scheduled', { id: refund.id, retryAt });
    } else {
      logger.info('stake job deleted since no retry', { id: refund.id });
      refundQueue.delete(refund.id);
    }
  }
};

export const refundQueue = createQueue<RefundJob>({
  name: 'refund',
  onJob: handleRefund,
  options: {
    concurrency: 1,
    maxRetries: 0,
    enableScheduledJob: true,
  },
});

export const startRefundQueue = async () => {
  const refunds = await Refund.findAll({ where: { status: ['pending'] } });
  refunds.forEach(async (x) => {
    const exist = await refundQueue.get(x.id);
    if (!exist) {
      refundQueue.push({ id: x.id, job: { refundId: x.id } });
      logger.info('Re-queued pending refund', { id: x.id });
    }
  });
};

refundQueue.on('failed', ({ id, job, error }) => {
  logger.error('refund job failed', { id, job, error });
});

events.on('refund.created', (refund: Refund) => {
  logger.info('schedule refund', { id: refund.id });
  refundQueue.push({ id: refund.id, job: { refundId: refund.id } });
});

/**
 * Handle onchain credit refund - burn user-held tokens for credit grants
 *
 * When a refund is issued for a credit purchase:
 * 1. Find credit grants associated with the invoice
 * 2. For each grant, check if its currency is an onchain credit
 * 3. Calculate how much to burn based on refund amount
 * 4. Only burn user-held tokens (remaining_amount in user's wallet)
 * 5. If user has consumed some credit, only burn what they have left
 *    (the consumed portion stays in system wallet as it corresponds to used services)
 * 6. Update credit grant status
 *
 * @param refund - The refund record
 * @param paymentDetails - Payment details to update on refund
 * @returns Total amount burned (as string), '0' if nothing to burn
 * @throws Error if burn operation fails
 */
/**
 * Handle onchain credit refund - burn user-held tokens for credit grants
 *
 * @param refund - The refund record
 * @returns Total amount burned (as string), undefined if nothing to burn
 * @throws Error if burn operation fails
 */
export async function handleOnchainCreditRefund(refund: Refund): Promise<string | undefined> {
  logger.info('Processing onchain credit refund', { refundId: refund.id, invoiceId: refund.invoice_id });

  // If no invoice_id, no credit grants to burn
  if (!refund.invoice_id) {
    return undefined;
  }

  const invoice = await Invoice.findByPk(refund.invoice_id);
  const customer = await Customer.findByPk(refund.customer_id);

  if (!invoice || !customer?.did) {
    throw new Error('Invoice or customer not found for refund');
  }

  const invoiceTotal = new BN(invoice.total || '0');
  if (invoiceTotal.lte(new BN(0))) {
    throw new Error(`Invoice ${refund.invoice_id} total is zero or negative`);
  }

  // Find ALL credit grants related to this invoice (not filtered by currency_id)
  const allGrants = await CreditGrant.findAll({
    where: {
      customer_id: refund.customer_id,
      status: ['pending', 'granted'],
    },
  });
  const creditGrants = allGrants.filter(
    (grant) => grant.metadata?.invoice_id === refund.invoice_id && CreditGrant.hasOnchainToken(grant)
  );

  if (creditGrants.length === 0) {
    logger.info('No active credit grants found for refund', { refundId: refund.id, invoiceId: refund.invoice_id });
    return undefined;
  }

  logger.info('Found credit grants to process for refund', {
    refundId: refund.id,
    creditGrantCount: creditGrants.length,
    invoiceTotal: invoice.total,
    refundAmount: refund.amount,
  });

  const refundAmount = new BN(refund.amount);
  let totalBurnedFromUser = new BN(0);
  let lastError: string | null = null;

  // Process each credit grant
  // eslint-disable-next-line no-restricted-syntax
  for (const creditGrant of creditGrants) {
    // Get the currency for THIS grant
    // eslint-disable-next-line no-await-in-loop
    const grantCurrency = await PaymentCurrency.findByPk(creditGrant.currency_id);

    // Verify currency has token config for burn operation
    if (!grantCurrency?.token_config) {
      logger.error('Payment currency token config not found for burn', {
        creditGrantId: creditGrant.id,
        currencyId: creditGrant.currency_id,
      });
      // eslint-disable-next-line no-continue
      continue;
    }

    const grantTotalAmount = new BN(creditGrant.amount);
    const grantRemainingAmount = new BN(creditGrant.remaining_amount || '0');

    // Calculate how much credit should be burned from this grant based on refund ratio
    // shouldBurnFromGrant = grantTotalAmount × refundAmount / invoiceTotal (multiply first to avoid precision loss)
    const shouldBurnFromGrant = grantTotalAmount.mul(refundAmount).div(invoiceTotal);

    // Actual burn is limited by what user actually has in their wallet
    const actualBurnAmount = BN.min(shouldBurnFromGrant, grantRemainingAmount);
    // The difference (if any) is consumed credit that stays in system wallet
    const systemRetainedAmount = shouldBurnFromGrant.sub(actualBurnAmount);

    logger.info('Processing credit grant for refund', {
      creditGrantId: creditGrant.id,
      currencyId: grantCurrency.id,
      grantTotal: grantTotalAmount.toString(),
      grantRemaining: grantRemainingAmount.toString(),
      shouldBurn: shouldBurnFromGrant.toString(),
      willBurnFromUser: actualBurnAmount.toString(),
      systemRetained: systemRetainedAmount.toString(),
    });

    let burnHash: string | null = null;
    let burnError: string | null = null;

    // Only burn if user has remaining tokens
    if (actualBurnAmount.gt(new BN(0))) {
      // Get the latest DID for burn (handles DID migration via on-chain state)
      // eslint-disable-next-line no-await-in-loop
      const customerState = await getAccountState(grantCurrency, customer.did);

      try {
        // eslint-disable-next-line no-await-in-loop
        burnHash = await burnToken({
          paymentCurrency: grantCurrency,
          amount: fromUnitToToken(actualBurnAmount.toString(), grantCurrency.decimal),
          sender: customerState?.address || customer.did,
          data: {
            reason: 'credit_grant_refund',
            creditGrantId: creditGrant.id,
            refundId: refund.id,
          },
        });

        totalBurnedFromUser = totalBurnedFromUser.add(actualBurnAmount);

        logger.info('Successfully burned user tokens for refund', {
          creditGrantId: creditGrant.id,
          burnHash,
          amount: actualBurnAmount.toString(),
        });

        // Emit burned event to trigger reconciliation
        events.emit('customer.credit_grant.burned', creditGrant, {
          burnHash,
          burnedAmount: actualBurnAmount.toString(),
        });
      } catch (error: any) {
        logger.error('Failed to burn user tokens for refund', {
          creditGrantId: creditGrant.id,
          refundId: refund.id,
          error: error.message,
        });
        burnError = error.message;
        lastError = error.message;
      }
    }

    // Update credit grant using new chain_detail field
    const newRemainingAmount = burnHash
      ? grantRemainingAmount.sub(actualBurnAmount).toString()
      : grantRemainingAmount.toString();
    const isFullyVoided = new BN(newRemainingAmount).eq(new BN(0));
    const chainDetail = creditGrant.chain_detail || {};

    // eslint-disable-next-line no-await-in-loop
    await creditGrant.update({
      status: isFullyVoided ? 'voided' : 'granted',
      remaining_amount: newRemainingAmount,
      voided_at: isFullyVoided ? Math.floor(Date.now() / 1000) : undefined,
      // eslint-disable-next-line no-nested-ternary
      chain_status: burnHash ? 'burn_completed' : burnError ? 'burn_failed' : undefined,
      chain_detail: {
        ...chainDetail,
        refund: {
          id: refund.id,
          burn_hash: burnHash || undefined,
          burned_amount: burnHash ? actualBurnAmount.toString() : '0',
          system_retained: systemRetainedAmount.toString(),
          burn_error: burnError || undefined,
        },
        ...(isFullyVoided ? { voided_reason: 'refund' } : {}),
      },
    });

    logger.info('Credit grant processed for refund', {
      creditGrantId: creditGrant.id,
      newStatus: isFullyVoided ? 'voided' : 'granted',
      newRemainingAmount,
      burnedFromUser: burnHash ? actualBurnAmount.toString() : '0',
      systemRetained: systemRetainedAmount.toString(),
    });
  }

  logger.info('Completed onchain credit refund processing', {
    refundId: refund.id,
    processedCount: creditGrants.length,
    totalBurnedFromUser: totalBurnedFromUser.toString(),
    refundAmount: refund.amount,
    hasError: !!lastError,
  });

  if (lastError) {
    throw new Error(lastError);
  }

  return totalBurnedFromUser.toString();
}
