import dayjs from 'dayjs';
import { Router } from 'express';
import Joi from 'joi';
import pick from 'lodash/pick';
import type { WhereOptions } from 'sequelize';

import { createListParamSchema, getOrder, MetadataSchema } from '../libs/api';
import { authenticate } from '../libs/security';
import { expandLineItems } from '../libs/session';
import { formatMetadata } from '../libs/util';
import { Price, Product, Subscription, SubscriptionItem, UsageRecord } from '../store/models';
import { forwardUsageRecordToStripe } from '../integrations/stripe/resource';
import { usageRecordQueue } from '../queues/usage-record';
import logger from '../libs/logger';

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

// FIXME: handle payment_behavior, proration_behavior
// @link https://stripe.com/docs/api/subscription_items/create
router.post('/', auth, async (req, res) => {
  const raw: Partial<SubscriptionItem> = pick(req.body, [
    'subscription_id',
    'price_id',
    'quantity',
    'billing_thresholds',
    'metadata',
  ]);
  const exist = await SubscriptionItem.findOne({
    where: { price_id: raw.price_id, subscription_id: raw.subscription_id },
  });
  if (exist) {
    return res.status(400).json({ error: `SubscriptionItem already exist: ${exist.id}` });
  }

  raw.livemode = req.livemode;
  if (raw.metadata) {
    const { error: metadataError } = MetadataSchema.validate(raw.metadata);
    if (metadataError) {
      return res.status(400).json({ error: `metadata invalid: ${metadataError.message}` });
    }
    raw.metadata = formatMetadata(raw.metadata);
  }

  const doc = await SubscriptionItem.create(raw as SubscriptionItem);
  logger.info('SubscriptionItem created', {
    id: doc.id,
    subscriptionId: doc.subscription_id,
    priceId: doc.price_id,
    quantity: doc.quantity,
    requestedBy: req.user?.did,
  });
  return res.json(doc);
});

// @link https://stripe.com/docs/api/subscription_items/list
const schema = createListParamSchema<{
  subscription_id: string;
  price_id: string;
}>({
  subscription_id: Joi.string(),
  price_id: Joi.string(),
});
router.get('/', auth, async (req, res) => {
  const { page, pageSize, ...query } = await schema.validateAsync(req.query, { stripUnknown: true });
  const where: WhereOptions<SubscriptionItem> = {};

  if (query.subscription_id) {
    where.subscription_id = query.subscription_id;
  }

  if (query.price_id) {
    where.price_id = query.price_id;
  }

  if (typeof query.livemode === 'boolean') {
    where.livemode = query.livemode;
  }

  try {
    const { rows, count } = await SubscriptionItem.findAndCountAll({
      where,
      order: getOrder(req.query, [['created_at', 'DESC']]),
      offset: (page - 1) * pageSize,
      limit: pageSize,
      include: [],
    });
    const list = rows.map((x) => x.toJSON());
    if (where.subscription_id) {
      // S1 optimization: Load only referenced prices (with product) instead of full table scans
      const priceIds = [...new Set(list.map((i: any) => i.price_id).filter(Boolean))];
      const pricesWithProduct =
        priceIds.length > 0
          ? await Price.findAll({ where: { id: priceIds }, include: [{ model: Product, as: 'product' }] })
          : [];
      const pricesJson = pricesWithProduct.map((x) => x.toJSON());
      const productMap = new Map<string, any>();
      pricesJson.forEach((p: any) => {
        if (p.product) productMap.set(p.product.id, p.product);
      });
      // @ts-ignore
      expandLineItems(list, Array.from(productMap.values()), pricesJson);
    }
    res.json({ count, list, paging: { page, pageSize } });
  } catch (err) {
    logger.error(err);
    res.json({ count: 0, list: [], paging: { page, pageSize } });
  }
});

// @link https://stripe.com/docs/api/subscription_items/retrieve
router.get('/:id', auth, async (req, res) => {
  try {
    const doc = await SubscriptionItem.findOne({
      where: { id: req.params.id },
      include: [{ model: Price, as: 'price' }],
    });
    if (doc) {
      res.json(doc);
    } else {
      res.status(404).json(null);
    }
  } catch (err) {
    logger.error(err);
    res.status(500).json({ error: `Failed to get subscription item: ${err.message}` });
  }
});

router.put('/:id', auth, async (req, res) => {
  const doc = await SubscriptionItem.findByPk(req.params.id);

  if (!doc) {
    return res.status(404).json({ error: `SubscriptionItem not found: ${req.params.id}` });
  }

  const updates: Partial<SubscriptionItem> = pick(req.body, ['price_id', 'quantity', 'billing_thresholds', 'metadata']);
  if (updates.price_id) {
    const exist = await SubscriptionItem.findOne({
      where: { price_id: updates.price_id, subscription_id: doc.subscription_id },
    });
    if (exist) {
      return res.status(400).json({ error: `SubscriptionItem already exist: ${exist.id}` });
    }

    if (!updates.quantity) {
      updates.quantity = 1;
    }
  }

  if (updates.metadata) {
    const { error: metadataError } = MetadataSchema.validate(updates.metadata);
    if (metadataError) {
      return res.status(400).json({ error: `metadata invalid: ${metadataError.message}` });
    }
    updates.metadata = formatMetadata(updates.metadata);
  }

  await doc.update(updates);
  logger.info('SubscriptionItem updated', {
    id: doc.id,
    subscriptionId: doc.subscription_id,
    updatedFields: Object.keys(updates),
    requestedBy: req.user?.did,
  });
  return res.json(doc);
});

// TODO: handle proration_behavior
// @link https://stripe.com/docs/api/subscription_items/delete
router.delete('/:id', auth, async (req, res) => {
  const doc = await SubscriptionItem.findByPk(req.params.id);

  if (!doc) {
    return res.status(404).json({ error: 'subscription item not found' });
  }

  if (req.body.clear_usage) {
    const price = await Price.findByPk(doc.price_id);
    if (price?.recurring?.usage_type === 'metered') {
      await UsageRecord.destroy({ where: { subscription_item_id: doc.id } });
    }
  }

  await doc.destroy();
  logger.info('SubscriptionItem deleted', {
    id: doc.id,
    subscriptionId: doc.subscription_id,
    clearUsage: req.body.clear_usage,
    requestedBy: req.user?.did,
  });
  return res.json(doc);
});

router.post('/:id/add-usage-quantity', auth, async (req, res) => {
  const { livemode } = req;
  if (livemode) {
    return res.status(403).json({ error: 'add usage quantity not allowed in livemode' });
  }

  const quantity = Number(req.body.quantity);
  if (quantity <= 0) {
    return res.status(400).json({ error: 'quantity must be positive' });
  }

  try {
    const subscriptionItem = await SubscriptionItem.findByPk(req.params.id);
    if (!subscriptionItem) {
      return res.status(404).json({ error: `SubscriptionItem(${req.params.id}) item not found` });
    }
    const subscription = await Subscription.findByPk(subscriptionItem.subscription_id);
    if (!subscription) {
      return res.status(400).json({ error: `Subscription not found: ${subscriptionItem.subscription_id}` });
    }

    if (subscription.isImmutable()) {
      return res.status(400).json({ error: `subscription(${subscription.id}) is immutable` });
    }

    const now = dayjs().unix();

    if (now < subscription.current_period_start || now > subscription.current_period_end) {
      return res.status(400).json({ error: 'timestamp must be within the current period' });
    }

    const usageRecord = await UsageRecord.create({
      livemode: Boolean(livemode),
      subscription_item_id: subscriptionItem.id,
      quantity,
      timestamp: now,
    } as UsageRecord);

    logger.info('Usage quantity added', {
      subscriptionItemId: subscriptionItem.id,
      subscriptionId: subscription.id,
      quantity,
      timestamp: now,
      requestedBy: req.user?.did,
    });

    if (subscription.billing_thresholds?.amount_gte) {
      usageRecordQueue.push({
        id: `usage-${subscription.id}`,
        job: { subscriptionId: subscription.id, subscriptionItemId: subscriptionItem.id },
      });
      logger.info('Usage record pushed to queue', {
        subscriptionId: subscription.id,
        subscriptionItemId: subscriptionItem.id,
      });
    }

    await forwardUsageRecordToStripe(subscriptionItem, {
      quantity,
      timestamp: now,
      action: 'increment',
    });
    logger.info('Usage record forwarded to Stripe', {
      subscriptionItemId: subscriptionItem.id,
      quantity,
      timestamp: now,
      action: 'increment',
    });

    return res.json(usageRecord);
  } catch (err) {
    logger.error(err);
    return res.status(400).json({ error: `Failed to add usage quantity: ${err.message}` });
  }
});

export default router;
