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

import dayjs from '../libs/dayjs';
import { getLock } from '../libs/lock';
import logger from '../libs/logger';
import createQueue from '../libs/queue';
import { getPriceUintAmountByCurrency } from '../libs/price';
import { Invoice, PaymentCurrency, Price, SubscriptionItem, TLineItemExpanded, UsageRecord } from '../store/models';
import { Subscription } from '../store/models/subscription';
import { invoiceQueue } from './invoice';
import { handleSubscriptionInvoice } from './subscription';

type UsageRecordJob = {
  subscriptionId: string;
  subscriptionItemId: string;
};

// We need to use lock here to prevent race conditions
export async function handleUsageRecord(job: UsageRecordJob) {
  const lock = getLock(`${job.subscriptionId}-threshold`);
  await lock.acquire();
  logger.info(`Lock acquired for subscription ${job.subscriptionId}`);
  const result = await doHandleUsageRecord(job);
  lock.release();
  logger.info(`Lock released for subscription ${job.subscriptionId}`);
  return result;
}

// generate invoice for metered billing
export const doHandleUsageRecord = async (job: UsageRecordJob) => {
  logger.info('handle usage record', job);

  const subscription = await Subscription.findByPk(job.subscriptionId);
  if (!subscription) {
    logger.warn('Subscription not found', job);
    return;
  }
  if (subscription.isActive() === false) {
    logger.warn('Subscription not active, so usage check is skipped', job);
    return;
  }
  if (!subscription.billing_thresholds?.amount_gte) {
    logger.warn('Subscription billing_threshold not set', job);
    return;
  }
  const currency = await PaymentCurrency.findByPk(subscription.currency_id);
  if (!currency) {
    logger.warn('Subscription currency not found', job);
    return;
  }

  const item = await SubscriptionItem.findByPk(job.subscriptionItemId);
  if (!item) {
    logger.warn('SubscriptionItem not found', job);
    return;
  }
  // @ts-ignore
  const lines = await Price.expand([{ id: item.id, price_id: item.price_id, quantity: item.quantity }], {
    product: true,
  });

  const filter = (x: TLineItemExpanded) => x.price.recurring?.usage_type === 'metered';
  const metered = lines.filter(filter);
  if (!metered.length) {
    logger.warn('SubscriptionItem not metered', job);
    return;
  }
  const [expanded] = metered;

  const start = subscription.current_period_start as number;
  const end = subscription.current_period_end as number;
  const latestThresholdInvoice = await Invoice.findOne({
    where: {
      subscription_id: subscription.id,
      billing_reason: 'subscription_threshold',
      period_start: start,
      period_end: end,
    },
    order: [['created_at', 'DESC']],
  });

  let usageStart = start;
  const usageEnd = dayjs().unix();
  if (latestThresholdInvoice && latestThresholdInvoice.metadata) {
    usageStart = latestThresholdInvoice.metadata.usage_end as number;
  }

  const rawQuantity = await UsageRecord.getSummary({
    // @ts-ignore
    id: expanded?.id as string,
    start: usageStart,
    end: usageEnd,
    method: expanded?.price.recurring?.aggregate_usage as any,
    dryRun: true,
  });
  // @ts-ignore
  const quantity = expanded?.price.transformQuantity(rawQuantity);
  const unitAmount = expanded?.price ? getPriceUintAmountByCurrency(expanded?.price, subscription.currency_id) : 0;
  const totalAmount = new BN(quantity).mul(new BN(unitAmount));
  const threshold = fromTokenToUnit(subscription.billing_thresholds.amount_gte, currency.decimal);
  logger.info('SubscriptionItem Usage check', {
    subscriptionId: subscription.id,
    subscriptionItemId: item.id,
    start: dayjs.unix(start).toISOString(),
    end: dayjs.unix(end).toISOString(),
    usageStart: dayjs.unix(usageStart).toISOString(),
    usageEnd: dayjs.unix(usageEnd).toISOString(),
    rawQuantity,
    quantity,

    unitAmount: fromUnitToToken(unitAmount || '0', currency.decimal),
    totalAmount: fromUnitToToken(totalAmount.toString(), currency.decimal),
    threshold: fromUnitToToken(threshold.toString(), currency.decimal),
  });
  if (totalAmount.lt(threshold)) {
    logger.info('SubscriptionItem usage below threshold', job);
    return;
  }

  logger.info('SubscriptionItem usage exceeds threshold', job);
  const invoice = await handleSubscriptionInvoice({
    subscription,
    filter, // include only metered items
    status: 'open',
    reason: 'threshold',
    start,
    end,
    offset: 0,
    usageStart,
    usageEnd,
    metadata: {
      usage_start: usageStart,
      usage_end: usageEnd,
    },
  });

  if (invoice) {
    // schedule invoice job
    invoiceQueue.push({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: true } });
    logger.info(`Invoice job scheduled on threshold: ${invoice.id}`);

    // persist invoice id
    await subscription.update({ latest_invoice_id: invoice.id });
    logger.info(`Subscription updated on threshold: ${subscription.id}`);
  }

  logger.info(`Usage record handling completed for subscription ${subscription.id}`);
};

export const usageRecordQueue = createQueue<UsageRecordJob>({
  name: 'usage-record',
  onJob: handleUsageRecord,
  options: {
    concurrency: 5,
    maxRetries: 3,
  },
});

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