/* eslint-disable consistent-return */
import { Request, Response, Router } from 'express';
import Joi from 'joi';
import pick from 'lodash/pick';
import { Op } from 'sequelize';

import { forwardUsageRecordToStripe } from '../integrations/stripe/resource';
import { createListParamSchema, getOrder } from '../libs/api';
import dayjs from '../libs/dayjs';
import { authenticate } from '../libs/security';
import { usageRecordQueue } from '../queues/usage-record';
import { Invoice, Price, Subscription, SubscriptionItem, UsageRecord } from '../store/models';
import logger from '../libs/logger';
import { getLock } from '../libs/lock';

const router = Router();
const auth = authenticate<UsageRecord>({ component: true, roles: ['owner', 'admin'] });

const UsageReportScheme = Joi.object({
  subscription_item_id: Joi.string().required(),
  quantity: Joi.number().required(),
  action: Joi.string().valid('increment', 'set').optional(),
  timestamp: Joi.number().optional(),
  livemode: Joi.boolean().optional(),
}).unknown(true);

// @link https://stripe.com/docs/api/usage_records/create
router.post('/', auth, async (req, res) => {
  const { error } = UsageReportScheme.validate(req.body);
  if (error) {
    return res.status(400).json({ error: error.message });
  }
  const raw: Partial<UsageRecord> = pick(req.body, ['timestamp', 'quantity', 'subscription_item_id']);
  const item = await SubscriptionItem.findByPk(raw.subscription_item_id);
  if (!item) {
    return res.status(400).json({ error: `SubscriptionItem not found: ${raw.subscription_item_id}` });
  }

  const subscription = await Subscription.findByPk(item.subscription_id);
  if (!subscription) {
    return res.status(400).json({ error: `Subscription not found: ${item.subscription_id}` });
  }
  if (raw.timestamp) {
    if (raw.timestamp < subscription.current_period_start || raw.timestamp > subscription.current_period_end) {
      return res.status(400).json({ error: '`timestamp` must be within the current period' });
    }
  }

  // @ts-ignore
  if (!raw.timestamp || raw.timestamp === 'now') {
    raw.timestamp = dayjs().unix();
  }

  const lock = getLock(`usage-record:${item.id}:${raw.timestamp}`);
  try {
    await lock.acquire();
    logger.info('Usage reporting', {
      subscriptionId: item.subscription_id,
      subscriptionItemId: item.id,
      body: req.body,
    });

    let doc = await UsageRecord.findOne({
      where: { timestamp: raw.timestamp, subscription_item_id: raw.subscription_item_id },
    });

    const action = req.body.action || 'increment';
    if (doc) {
      if (doc.billed) {
        logger.info('UsageRecord updated', {
          subscriptionItemId: raw.subscription_item_id,
          timestamp: raw.timestamp,
          newQuantity: raw.quantity,
        });
        return res.status(400).json({ error: 'UsageRecord is immutable because already billed' });
      }
      if (action === 'increment') {
        await doc.increment('quantity', { by: raw.quantity });
        logger.info('UsageRecord incremented', {
          subscriptionItemId: raw.subscription_item_id,
          timestamp: raw.timestamp,
          incrementBy: raw.quantity,
          action,
        });
      } else {
        if (subscription.billing_thresholds?.amount_gte) {
          logger.warn('Invalid action for subscription with billing_thresholds', {
            subscriptionId: subscription.id,
            action,
          });
          return res
            .status(400)
            .json({ error: 'UsageRecord action must be `increment` for subscriptions with billing_thresholds' });
        }
        await doc.update({ quantity: raw.quantity });
        logger.info('UsageRecord updated', {
          subscriptionItemId: raw.subscription_item_id,
          timestamp: raw.timestamp,
          newQuantity: raw.quantity,
          action,
        });
      }
    } else {
      raw.livemode = req.livemode;
      doc = await UsageRecord.create(raw as UsageRecord);
    }

    if (subscription.billing_thresholds?.amount_gte) {
      usageRecordQueue.push({
        id: `usage-${subscription.id}`,
        job: { subscriptionId: subscription.id, subscriptionItemId: item.id },
      });
      logger.info('UsageRecord pushed to queue', {
        subscriptionId: subscription.id,
        subscriptionItemId: item.id,
      });
    }
    await forwardUsageRecordToStripe(item, {
      quantity: Number(raw.quantity),
      timestamp: raw.timestamp,
      action,
    });
    logger.info('UsageRecord forwarded to Stripe', {
      subscriptionItemId: item.id,
      quantity: Number(raw.quantity),
      timestamp: raw.timestamp,
      action,
    });
    await doc.reload();
    return res.json(doc);
  } catch (err) {
    logger.error('Error in usage-records', { error: err });
    return res.status(400).json({ error: err.message || 'UsageRecord creation failed' });
  } finally {
    lock.release();
  }
});

// @link https://stripe.com/docs/api/usage_records/subscription_item_summary_list
const schema = createListParamSchema<{
  subscription_item_id: string;
  start?: number;
  end?: number;
}>(
  {
    subscription_item_id: Joi.string().required(),
    start: Joi.number().optional(),
    end: Joi.number().optional(),
  },
  100
);
router.get('/summary', auth, async (req, res) => {
  const { page, pageSize, ...query } = await schema.validateAsync(req.query, { stripUnknown: true });

  try {
    const item = await SubscriptionItem.findByPk(query.subscription_item_id, {
      include: [{ model: Price, as: 'price' }],
    });
    if (!item) {
      return res.status(400).json({ error: `SubscriptionItem not found: ${query.subscription_item_id}` });
    }

    const { rows, count } = await Invoice.findAndCountAll({
      where: { subscription_id: item.subscription_id },
      attributes: ['id', 'period_end', 'period_start'],
      order: getOrder(req.query, [['created_at', 'DESC']]),
      offset: (page - 1) * pageSize,
      limit: pageSize,
    });

    const list = await Promise.all(
      rows.map(async (invoice) => {
        return {
          livemode: invoice.livemode,
          invoice_id: invoice.id,
          subscription_item_id: item.id,
          total_usage: await UsageRecord.getSummary({
            id: item.id,
            start: invoice.period_start,
            end: invoice.period_end,
            // @ts-ignore
            method: item.price.recurring.aggregate_usage,
            dryRun: true,
          }),
          period: {
            start: invoice.period_start,
            end: invoice.period_end,
          },
        };
      })
    );

    res.json({ count, list, paging: { page, pageSize } });
  } catch (err) {
    logger.error(err);
    res.json({ count: 0, list: [], paging: { page, pageSize } });
  }
});

const UsageRecordScheme = Joi.object({
  subscription_item_id: Joi.string().required(),
  start: Joi.number().optional(),
  end: Joi.number().optional(),
  livemode: Joi.boolean().empty('').optional(),
  q: Joi.string().empty('').optional(), // query
  o: Joi.string().empty('').optional(), // order
}).unknown(true);

export function createUsageRecordQueryFn(doc?: Subscription) {
  return async (req: Request, res: Response) => {
    const { error, value: query } = UsageRecordScheme.validate(req.query, { stripUnknown: true });
    if (error) {
      return res.status(400).json({ error: `usage record request query invalid: ${error.message}` });
    }
    try {
      const item = await SubscriptionItem.findByPk(query.subscription_item_id);
      if (!item) {
        return res.status(400).json({ error: `SubscriptionItem not found: ${query.subscription_item_id}` });
      }
      const subscription = doc || (await Subscription.findByPk(item.subscription_id));
      if (!subscription) {
        return res.status(400).json({ error: `Subscription not found: ${item.subscription_id}` });
      }

      const { rows: list, count } = await UsageRecord.findAndCountAll({
        where: {
          subscription_item_id: query.subscription_item_id,
          timestamp: {
            [Op.gt]: query.start || subscription.current_period_start,
            [Op.lte]: query.end || subscription.current_period_end,
          },
        },
        order: [['created_at', 'ASC']],
      });

      res.json({ count, list });
    } catch (err) {
      logger.error(err);
      res.json({ count: 0, list: [] });
    }
  };
}

router.get('/', auth, createUsageRecordQueryFn());

export default router;
