import dayjs from '../libs/dayjs';
import { events } from '../libs/event';
import logger from '../libs/logger';
import { getGasPayerExtra } from '../libs/payment';
import createQueue from '../libs/queue';
import { wallet, ethWallet } from '../libs/auth';
import { sendErc20ToUser } from '../integrations/ethereum/token';
import { PaymentMethod } from '../store/models/payment-method';
import { PaymentCurrency } from '../store/models/payment-currency';
import { Payout } from '../store/models/payout';
import { EVM_CHAIN_TYPES } from '../libs/constants';
import type { PaymentError } from '../store/models/types';
import { getNextRetry, MAX_RETRY_COUNT } from '../libs/util';

type PayoutJob = {
  payoutId: string;
  retryOnError?: boolean;
};

type ValidationResult =
  | { valid: false }
  | { valid: true; payout: Payout; paymentMethod: PaymentMethod; paymentCurrency: PaymentCurrency };

// Validate payout and fetch required data
async function validatePayoutAndFetchData(job: PayoutJob): Promise<ValidationResult> {
  const payout = await Payout.findByPk(job.payoutId);
  if (!payout) {
    logger.warn('Payout not found', { id: job.payoutId });
    return { valid: false };
  }

  if (payout.status !== 'pending') {
    logger.warn('Payout status not expected', { id: payout.id, status: payout.status });
    return { valid: false };
  }

  const paymentMethod = await PaymentMethod.findByPk(payout.payment_method_id);
  if (!paymentMethod) {
    logger.warn('PaymentMethod not found', { id: payout.payment_method_id });
    return { valid: false };
  }

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

  return {
    valid: true,
    payout,
    paymentMethod,
    paymentCurrency,
  };
}

// Process Arcblock chain payout
async function processArcblockPayout(payout: Payout, paymentMethod: PaymentMethod, paymentCurrency: PaymentCurrency) {
  const client = paymentMethod.getOcapClient();

  const signed = await client.signTransferV2Tx({
    tx: {
      itx: {
        to: payout.destination,
        value: '0',
        assets: [],
        tokens: [{ address: paymentCurrency.contract, value: payout.amount }],
        data: {
          typeUrl: 'json',
          // @ts-ignore Type issue, won't affect server runtime
          value: {
            appId: wallet.address,
            reason: 'payout',
            payoutId: payout.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('Payout completed', { id: payout.id, txHash });

  await payout.update({
    status: 'paid',
    last_attempt_error: null,
    attempt_count: payout.attempt_count + 1,
    attempted: true,
    payment_details: {
      arcblock: {
        tx_hash: txHash,
        payer: wallet.address,
        type: 'transfer',
      },
    },
  });
}

// Process EVM chain payout
async function processEvmPayout(payout: Payout, paymentMethod: PaymentMethod, paymentCurrency: PaymentCurrency) {
  if (!paymentCurrency.contract) {
    throw new Error('Payout not supported for ethereum payment currencies without contract');
  }

  const client = paymentMethod.getEvmClient();
  const paymentType = paymentMethod.type;

  // Send ERC20 tokens from system wallet to user address
  const receipt = await sendErc20ToUser(client, paymentCurrency.contract, payout.destination, payout.amount);

  logger.info('Payout completed', { id: payout.id, txHash: receipt.hash });

  await payout.update({
    status: 'paid',
    last_attempt_error: null,
    attempt_count: payout.attempt_count + 1,
    attempted: true,
    payment_details: {
      [paymentType]: {
        tx_hash: receipt.hash,
        payer: ethWallet.address,
        block_height: receipt.blockNumber.toString(),
        gas_used: receipt.gasUsed.toString(),
        gas_price: receipt.gasPrice.toString(),
        type: 'transfer',
      },
    },
  });
}

// Handle payout failure with retry logic
async function handlePayoutFailure(payout: Payout, paymentMethod: PaymentMethod, error: any, retryOnError: boolean) {
  const paymentError: PaymentError = {
    type: 'card_error',
    code: error.code,
    message: error.message,
    payment_method_id: paymentMethod.id,
    payment_method_type: paymentMethod.type,
  };

  if (!retryOnError) {
    // Mark as failed without retry
    await payout.update({
      status: 'failed',
      last_attempt_error: paymentError,
      attempt_count: payout.attempt_count + 1,
      attempted: true,
      failure_message: error.message,
    });
    return;
  }

  const attemptCount = payout.attempt_count + 1;

  if (attemptCount >= MAX_RETRY_COUNT) {
    // Exceeded max retry count
    await payout.update({
      status: 'failed',
      last_attempt_error: paymentError,
      attempt_count: attemptCount,
      attempted: true,
      failure_message: error.message,
    });
    logger.info('Payout job deleted since max retry exceeded', { id: payout.id });
    payoutQueue.delete(payout.id);
    return;
  }

  const nextAttempt = getNextRetry(attemptCount);
  await payout.update({
    status: 'pending',
    last_attempt_error: paymentError,
    attempt_count: attemptCount,
    attempted: true,
    next_attempt: nextAttempt,
  });

  payoutQueue.push({
    id: payout.id,
    job: { payoutId: payout.id, retryOnError: true },
    runAt: nextAttempt,
  });

  logger.error('Payout retry scheduled', { id: payout.id, nextAttempt, retryCount: attemptCount });
}

// Process payout transaction
export const handlePayout = async (job: PayoutJob) => {
  logger.info('handle payout', job);

  const result = await validatePayoutAndFetchData(job);
  if (!result.valid) {
    return;
  }

  const { payout, paymentMethod, paymentCurrency } = result;

  logger.info('Payout attempt', { id: payout.id, attempt: payout.attempt_count });
  try {
    await payout.update({ status: 'in_transit', last_attempt_error: null });
    logger.info('Payout status updated to in_transit', { payoutId: payout.id });

    if (paymentMethod.type === 'arcblock') {
      await processArcblockPayout(payout, paymentMethod, paymentCurrency);
    } else if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
      await processEvmPayout(payout, paymentMethod, paymentCurrency);
    }
  } catch (err) {
    logger.error('Payout failed', { error: err, id: payout.id });
    await handlePayoutFailure(payout, paymentMethod, err, !!job.retryOnError);
  }
};

// Create queue processor
export const payoutQueue = createQueue<PayoutJob>({
  name: 'payout',
  onJob: handlePayout,
  options: {
    concurrency: 1,
    maxRetries: 0,
    enableScheduledJob: true,
  },
});

// Handle queue failure events
payoutQueue.on('failed', ({ id, job, error }) => {
  logger.error('Payout job failed', { id, job, error });
});

// Start queue, find all payouts with "pending" status
export const startPayoutQueue = async () => {
  const payouts = await Payout.findAll({
    where: {
      status: 'pending',
    },
  });

  payouts.forEach(async (payout) => {
    const exist = await payoutQueue.get(payout.id);
    if (!exist) {
      // Use next attempt time if set
      if (payout.next_attempt && payout.next_attempt > dayjs().unix()) {
        payoutQueue.push({
          id: payout.id,
          job: { payoutId: payout.id, retryOnError: true },
          runAt: payout.next_attempt,
        });
      } else {
        payoutQueue.push({
          id: payout.id,
          job: { payoutId: payout.id, retryOnError: true },
        });
      }
    }
  });
};

// Listen for newly created payouts
events.on('payout.created', async (payout: Payout) => {
  if (payout.status === 'pending') {
    const exist = await payoutQueue.get(payout.id);
    if (!exist) {
      payoutQueue.push({
        id: payout.id,
        job: { payoutId: payout.id, retryOnError: true },
      });
    }
  }
});

// Add synchronous payout processing event
events.on('payout.queued', async (id, job, args = {}) => {
  const { sync, ...extraArgs } = args;
  if (sync) {
    try {
      await payoutQueue.pushAndWait({
        id,
        job,
        ...extraArgs,
      });
      events.emit('payout.queued.done', { id, job });
    } catch (error) {
      logger.error('Error in payout.queued', { id, job, error });
      events.emit('payout.queued.error', { id, job, error });
    }
    return;
  }
  try {
    const existJob = await payoutQueue.get(id);
    if (existJob) {
      await payoutQueue.delete(id);
      logger.info('Removed existing payout job for immediate execution', {
        id,
        originalRunAt: existJob.runAt,
      });
    }
    payoutQueue.push({
      id,
      job,
      ...extraArgs,
    });
    events.emit('payout.queued.done', { id, job });
  } catch (error) {
    logger.error('Error in payout.queued', { id, job, error });
    events.emit('payout.queued.error', { id, job, error });
  }
});
