/* eslint-disable no-await-in-loop */
import fs from 'fs';
import path from 'path';

import { BN, fromUnitToToken } from '@ocap/util';
import { Op } from 'sequelize';

import dayjs from '../libs/dayjs';
import env from '../libs/env';
import logger from '../libs/logger';
import {
  GroupedBN,
  PaymentCurrency,
  PaymentIntent,
  PaymentStat,
  Payout,
  Refund,
  TPaymentCurrency,
} from '../store/models';

export function groupByCurrency(items: { amount: string; currency_id: string }[]) {
  return items.reduce((acc: GroupedBN, { amount, currency_id }) => {
    if (!acc[currency_id]) {
      acc[currency_id] = '0';
    }
    acc[currency_id] = new BN(acc[currency_id]).add(new BN(amount)).toString();
    return acc;
  }, {});
}

export function mergeResults(result: GroupedBN, currencies: TPaymentCurrency[]): GroupedBN {
  return currencies.reduce((acc, currency) => {
    acc[currency.id] = fromUnitToToken(result[currency.id] || '0', currency.decimal);
    return acc;
  }, {} as GroupedBN);
}

export async function getPaymentStat(
  date: string
): Promise<{ timestamp: number; stats: { [key: string]: GroupedBN }; currencies: TPaymentCurrency[] }> {
  const start = dayjs(date).startOf('day');
  const end = dayjs(date).add(1, 'day').startOf('day');

  // group paymentIntents by currency_id
  const [currencies, payments, payouts, refunds] = await Promise.all([
    PaymentCurrency.findAll(),
    groupByCurrency(
      await PaymentIntent.findAll({
        where: {
          updated_at: { [Op.gte]: start.toDate(), [Op.lt]: end.toDate() },
          status: 'succeeded',
        },
        attributes: ['amount', 'currency_id'],
      })
    ),
    groupByCurrency(
      await Payout.findAll({
        where: {
          updated_at: { [Op.gte]: start.toDate(), [Op.lt]: end.toDate() },
          status: 'paid',
        },
        attributes: ['amount', 'currency_id'],
      })
    ),
    groupByCurrency(
      await Refund.findAll({
        where: {
          updated_at: { [Op.gte]: start.toDate(), [Op.lt]: end.toDate() },
          type: 'refund',
          status: 'succeeded',
        },
        attributes: ['amount', 'currency_id'],
      })
    ),
  ]);

  return {
    timestamp: start.unix(),
    stats: {
      payment: mergeResults(payments, currencies),
      payout: mergeResults(payouts, currencies),
      refund: mergeResults(refunds, currencies),
    },
    currencies,
  };
}

export async function getDates() {
  const item = await PaymentStat.findOne({
    order: [['timestamp', 'DESC']],
    limit: 1,
    offset: 0,
    attributes: ['timestamp'],
  });

  const dayInSeconds = 60 * 60 * 24;
  const now = dayjs().unix() - dayInSeconds;

  if (item) {
    const dates: string[] = [];
    let current = item.timestamp + dayInSeconds;
    while (current < now) {
      dates.push(
        dayjs(current * 1000)
          .toDate()
          .toISOString()
      );
      current += dayInSeconds;
    }

    return dates;
  }

  return [
    dayjs(now * 1000)
      .subtract(1, 'day')
      .toDate()
      .toISOString(),
  ];
}

export async function createPaymentStat(date?: string) {
  const dates = date ? [date] : await getDates();
  await Promise.all(
    // eslint-disable-next-line @typescript-eslint/no-shadow
    dates.map(async (date) => {
      const { stats, timestamp, currencies } = await getPaymentStat(date);
      const currencyIds = currencies.map((currency) => currency.id);
      const existingStats = await PaymentStat.findAll({
        where: {
          timestamp,
          currency_id: { [Op.in]: currencyIds },
        },
        attributes: ['id', 'currency_id'],
      });
      const existingMap = existingStats.reduce<Record<string, string>>((acc, item) => {
        acc[item.currency_id] = item.id;
        return acc;
      }, {});
      const records = currencies.map((currency) => {
        const base = {
          livemode: currency.livemode,
          timestamp,
          currency_id: currency.id,
          amount_paid: stats.payment?.[currency.id] || '0',
          amount_payout: stats.payout?.[currency.id] || '0',
          amount_refund: stats.refund?.[currency.id] || '0',
        } as any;
        const existingId = existingMap[currency.id];
        if (existingId) {
          base.id = existingId;
        }
        return base;
      });
      await PaymentStat.bulkCreate(records, {
        updateOnDuplicate: ['amount_paid', 'amount_payout', 'amount_refund'],
      });
      logger.info('PaymentStat synced', { date, timestamp, count: records.length });
    })
  );
}

export async function ensurePaymentStats() {
  const lock = path.join(env.dataDir, '.payment_stat.lock');
  if (fs.existsSync(lock)) {
    return;
  }

  const firstPayment = await PaymentIntent.findOne({ order: [['created_at', 'ASC']], attributes: ['created_at'] });
  if (firstPayment) {
    const now = dayjs().unix();
    for (let current = dayjs(firstPayment.created_at); current.unix() < now; current = current.add(1, 'day')) {
      await createPaymentStat(current.toDate().toISOString());
    }
    fs.writeFileSync(lock, '');
  } else {
    logger.warn('No payment found, skip payment stat migration');
  }
}
