import { fromTokenToUnit, fromUnitToToken } from '@ocap/util';
import { Router } from 'express';
import Joi from 'joi';
import pick from 'lodash/pick';
import type { WhereOptions } from 'sequelize';

import { createListParamSchema, getOrder, getWhereFromQuery, MetadataSchema } from '../libs/api';
import { getExchangeRateService } from '../libs/exchange-rate';
import { getExchangeRateSymbol, hasTokenAddress } from '../libs/exchange-rate/token-address-mapping';
import logger from '../libs/logger';
import { authenticate } from '../libs/security';
import { canUpsell } from '../libs/session';
import { PaymentCurrency } from '../store/models/payment-currency';
import { Price } from '../store/models/price';
import { Product } from '../store/models/product';
import { checkCurrencySupportRecurring } from '../libs/product';
import { ChainType, Meter, PaymentMethod } from '../store/models';

const router = Router();

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

// Schedule configuration for credit delivery
const CreditScheduleConfigSchema = Joi.object({
  enabled: Joi.boolean().default(false),
  delivery_mode: Joi.string().valid('invoice', 'schedule').default('invoice'),
  interval_value: Joi.number().min(0.01).when('enabled', {
    is: true,
    then: Joi.required(),
    otherwise: Joi.optional(),
  }),
  interval_unit: Joi.string().valid('hour', 'day', 'week', 'month').when('enabled', {
    is: true,
    then: Joi.required(),
    otherwise: Joi.optional(),
  }),
  amount_per_grant: Joi.number().greater(0).when('enabled', {
    is: true,
    then: Joi.required(),
    otherwise: Joi.optional(),
  }),
  first_grant_timing: Joi.string().valid('immediate', 'after_trial', 'after_first_payment').default('immediate'),
  expire_with_next_grant: Joi.boolean().default(true),
  max_grants_per_period: Joi.number().min(1).optional(),
});

const CreditConfigSchema = Joi.object({
  valid_duration_value: Joi.number().default(0).optional(),
  valid_duration_unit: Joi.string().valid('hours', 'days', 'weeks', 'months', 'years').default('days').optional(),
  priority: Joi.number().min(0).max(100).default(50).optional(),
  applicable_prices: Joi.array().items(Joi.string()).default([]).optional(),
  // credit_amount is required for one-time delivery, optional when schedule is enabled
  credit_amount: Joi.number().greater(0).when('schedule.enabled', {
    is: true,
    then: Joi.optional(),
    otherwise: Joi.required(),
  }),
  currency_id: Joi.string().required(),
  schedule: CreditScheduleConfigSchema.optional(),
})
  .custom((value, helpers) => {
    if (value?.schedule?.expire_with_next_grant && (value?.valid_duration_value || 0) > 0) {
      return helpers.error('any.invalid');
    }
    return value;
  }, 'credit config validation')
  .messages({
    'any.invalid': 'valid_duration_* is mutually exclusive with schedule.expire_with_next_grant',
  });

const exchangeRateService = getExchangeRateService();

async function validateDynamicPricingCurrencies(currencyIds: string[]): Promise<string | null> {
  if (!currencyIds?.length) {
    return null;
  }

  const uniqueIds = Array.from(new Set(currencyIds.filter(Boolean)));
  const currencies = (await PaymentCurrency.findAll({
    where: { id: uniqueIds },
    include: [{ model: PaymentMethod, as: 'payment_method' }],
  })) as (PaymentCurrency & { payment_method: PaymentMethod })[];
  const unsupported: string[] = [];

  await Promise.all(
    currencies.map(async (currency) => {
      const rateSymbol = getExchangeRateSymbol(currency.symbol, currency.payment_method?.type as ChainType);
      if (currency.payment_method.type === 'stripe') {
        return;
      }
      if (!hasTokenAddress(rateSymbol)) {
        unsupported.push(`${currency.symbol}: missing token mapping`);
        return;
      }
      try {
        await exchangeRateService.getRate(rateSymbol);
      } catch (error: any) {
        unsupported.push(`${currency.symbol}: ${error.message}`);
      }
    })
  );

  return unsupported.length ? `Exchange rate not available for ${unsupported.join(', ')}` : null;
}

export async function getExpandedPrice(id: string) {
  const price = await Price.findByPkOrLookupKey(id, {
    include: [
      { model: Product, as: 'product' },
      { model: PaymentCurrency, as: 'currency' },
    ],
  });

  if (price) {
    const currencies = await PaymentCurrency.findAll();
    const doc = Price.formatAfterRead(price.toJSON(), currencies);

    if (doc.upsell?.upsells_to_id) {
      const to = await Price.findByPk(doc.upsell.upsells_to_id);
      if (to) {
        // @ts-ignore
        doc.upsell.upsells_to = Price.formatAfterRead(to.toJSON(), currencies);
      }
    }

    if (doc.recurring?.meter_id) {
      // @ts-ignore
      doc.meter = await Meter.findByPk(doc.recurring?.meter_id);
    }
    return doc;
  }

  return null;
}

// list prices
const paginationSchema = createListParamSchema<{
  active?: boolean;
  type?: string;
  currency_id?: string;
  product_id?: string;
  lookup_key?: string;
}>({
  active: Joi.boolean().empty(''),
  type: Joi.string().empty(''),
  currency_id: Joi.string().empty(''),
  product_id: Joi.string().empty(''),
  lookup_key: Joi.string().empty(''),
});
router.get('/', auth, async (req, res) => {
  const { page, pageSize, active, livemode, ...query } = await paginationSchema.validateAsync(req.query, {
    stripUnknown: false,
    allowUnknown: true,
  });
  const where: WhereOptions<Price> = {};

  if (typeof active === 'boolean') {
    where.active = active;
  }
  if (typeof livemode === 'boolean') {
    where.livemode = livemode;
  }
  ['type', 'currency_id', 'product_id', 'lookup_key'].forEach((key: string) => {
    // @ts-ignore
    if (query[key]) {
      // @ts-ignore
      where[key] = query[key].split(',').map((x: string) => x.trim()).filter(Boolean); // prettier-ignore
    }
  });

  Object.keys(query)
    .filter((x) => x.startsWith('recurring.'))
    .forEach((key: string) => {
      // @ts-ignore
      where[key] = query[key];
    });

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

  res.json({ count, list: await Promise.all(rows.map((x) => getExpandedPrice(x.id))), paging: { page, pageSize } });
});

// search prices
const searchSchema = createListParamSchema<{
  query: string;
}>({
  query: Joi.string(),
});
router.get('/search', auth, async (req, res) => {
  const { page, pageSize, query, livemode } = await searchSchema.validateAsync(req.query, {
    stripUnknown: false,
    allowUnknown: true,
  });

  const where = getWhereFromQuery(query);
  if (typeof livemode === 'boolean') {
    where.livemode = livemode;
  }
  const { rows, count } = await Price.findAndCountAll({
    where,
    attributes: ['id'],
    order: getOrder(req.query, [['created_at', 'DESC']]),
    offset: (page - 1) * pageSize,
    limit: pageSize,
  });

  res.json({ count, list: await Promise.all(rows.map((x) => getExpandedPrice(x.id))) });
});

export async function createPrice(payload: any) {
  const raw: Price & { model: 'string' } = payload;
  raw.active = true;
  raw.locked = false;
  raw.livemode = !!payload.livemode;
  raw.currency_id = payload.currency_id;

  if (!raw.product_id) {
    throw new Error('product_id is required to create a price');
  }

  if (!raw.unit_amount) {
    throw new Error('price unit_amount is required');
  }

  const product = await Product.findByPk(raw.product_id);
  if (!product) {
    throw new Error(`product ${raw.product_id} not found for price`);
  }

  if (product.type === 'credit') {
    const creditConfig = raw.metadata.credit_config;
    if (!creditConfig) {
      throw new Error('credit_config is required');
    }
    const { error, value: creditConfigValue } = CreditConfigSchema.validate(creditConfig);
    if (error) {
      throw new Error(`credit_config is invalid: ${error.message}`);
    }
    raw.metadata.credit_config = creditConfigValue;
  }

  const currencies = await PaymentCurrency.findAll({ where: { active: true } });
  const currency = currencies.find((x) => x.id === raw.currency_id);
  if (!currency) {
    throw new Error(`currency used in price or not active: ${raw.currency_id}`);
  }

  if (Array.isArray(raw.currency_options) === false) {
    raw.currency_options = [];
  }
  if (raw.currency_options.some((x) => x.currency_id === raw.currency_id) === false) {
    raw.currency_options.unshift({
      currency_id: raw.currency_id,
      unit_amount: raw.unit_amount,
      tiers: null,
      custom_unit_amount: null,
    });
  }

  raw.currency_options = Price.formatCurrencies(raw.currency_options, currencies);
  raw.unit_amount = fromTokenToUnit(raw.unit_amount, currency.decimal).toString();
  const isRecurring = payload.type === 'recurring';
  const { notSupportCurrencies, validate } = await checkCurrencySupportRecurring(
    (raw.currency_options || [])?.map((x) => x.currency_id).filter(Boolean),
    isRecurring
  );
  if (!validate) {
    throw new Error(`currency ${notSupportCurrencies.map((x) => x.name).join(', ')} does not support recurring`);
  }
  if (isRecurring && raw.recurring && !raw.recurring.usage_type) {
    raw.recurring.usage_type = 'licensed';
  }
  const price = await Price.insert(raw);
  return getExpandedPrice(price.id as string);
}

const priceQuantitySchema = Joi.object({
  quantity_available: Joi.number().integer().min(0).optional().default(0),
  quantity_limit_per_checkout: Joi.number().integer().min(0).optional().default(0),
});

const priceAmountSchema = Joi.object({
  currency_options: Joi.array()
    .items(
      Joi.object({
        unit_amount: Joi.number().greater(0).optional(),
        // 其他属性
      }).unknown(true)
    )
    .optional(),
  unit_amount: Joi.number().greater(0).optional(),
  nickname: Joi.string().max(64).empty('').optional(),
  lookup_key: Joi.string().max(64).empty('').optional(),
});

// FIXME: @wangshijun use schema validation
// create price
// eslint-disable-next-line consistent-return
router.post('/', auth, async (req, res) => {
  try {
    const { error } = priceQuantitySchema.validate(
      pick(req.body, ['quantity_available', 'quantity_limit_per_checkout'])
    );
    if (error) {
      return res.status(400).json({ error: `Price create request invalid: ${error.message}` });
    }
    const { error: priceAmountError } = priceAmountSchema.validate(
      pick(req.body, ['currency_options', 'unit_amount', 'nickname', 'lookup_key'])
    );
    if (priceAmountError) {
      return res.status(400).json({ error: `Price create request invalid: ${priceAmountError.message}` });
    }
    if (req.body.metadata) {
      const { error: metadataError } = MetadataSchema.validate(req.body.metadata);
      if (metadataError) {
        return res.status(400).json({ error: `metadata invalid: ${metadataError.message}` });
      }
    }

    if (req.body.pricing_type === 'dynamic') {
      const currencyIds = req.body.currency_options?.map((x: any) => x.currency_id).filter(Boolean) || [];
      if (req.body.currency_id) {
        currencyIds.push(req.body.currency_id);
      }
      const validationError = await validateDynamicPricingCurrencies(currencyIds);
      if (validationError) {
        return res.status(400).json({ error: validationError });
      }
    }

    const result = await createPrice({
      ...req.body,
      livemode: !!req.livemode,
      currency_id: req.body.currency_id || req.baseCurrency.id,
      created_via: req.user?.via as string,
      quantity_sold: 0,
    });

    logger.info(`Price created: ${result?.id}`, { priceId: result?.id, requestedBy: req.user?.did });
    res.json(result);
  } catch (err) {
    logger.error('Error creating price', { error: err.message, request: req.body });
    res.status(400).json({ error: err.message });
  }
});

// get price detail
router.get('/:id', auth, async (req, res) => {
  const doc = await getExpandedPrice(req.params.id as string);
  if (doc) {
    res.json(doc);
  } else {
    res.status(404).json(null);
  }
});

// get price used status
router.get('/:id/used', auth, async (req, res) => {
  const price = await Price.findByPkOrLookupKey(req.params.id as string);
  if (!price) {
    return res.status(404).json({ error: 'Price not found' });
  }

  const used = await price.isUsed(true);
  if (!used) {
    await price.update({ locked: false });
  }

  return res.json({ used });
});

router.get('/:id/upsell', auth, async (req, res) => {
  const price = await Price.findByPkOrLookupKey(req.params.id as string, {
    include: [{ model: PaymentCurrency, as: 'currency' }],
  });

  if (price) {
    const prices = await Price.findAll({
      where: { product_id: price.product_id },
      include: [{ model: PaymentCurrency, as: 'currency' }],
    });
    const upsells = prices.filter((x) => canUpsell(price, x));
    res.json(upsells);
  } else {
    res.json(null);
  }
});

// update price
// FIXME: upsell validate https://stripe.com/docs/payments/checkout/upsells
router.put('/:id', auth, async (req, res) => {
  const quantityKeys = ['quantity_available', 'quantity_limit_per_checkout'];
  const { error } = priceQuantitySchema.validate(pick(req.body, quantityKeys));
  if (error) {
    return res.status(400).json({ error: `Price update request invalid: ${error.message}` });
  }

  const { error: priceAmountError } = priceAmountSchema.validate(
    pick(req.body, ['currency_options', 'unit_amount', 'nickname', 'lookup_key'])
  );
  if (priceAmountError) {
    return res.status(400).json({ error: `Price update request invalid: ${priceAmountError.message}` });
  }
  const doc = await Price.findByPkOrLookupKey(req.params.id as string);

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

  const product = await Product.findByPk(doc.product_id);
  if (!product) {
    return res.status(404).json({ error: 'product not found' });
  }

  if (doc.active === false) {
    return res.status(403).json({ error: 'price archived' });
  }

  if (Number(req.body.quantity_available) > 0 && Number(req.body.quantity_available) < Number(doc.quantity_sold)) {
    // 可售数量不得小于已售数量
    return res.status(400).json({ error: 'the available quantity cannot be less than the quantity sold' });
  }

  const locked = doc.locked && process.env.PAYMENT_CHANGE_LOCKED_PRICE !== '1';
  const { error: metadataError } = MetadataSchema.validate(req.body.metadata);
  if (metadataError) {
    return res.status(400).json({ error: `metadata invalid: ${metadataError.message}` });
  }
  const updates: Partial<Price> = Price.formatBeforeSave(
    pick(
      req.body,
      locked
        ? ['nickname', 'description', 'metadata', 'upsell', 'lookup_key', ...quantityKeys]
        : [
            'type',
            'model',
            'active',
            'livemode',
            'nickname',
            'recurring',
            'description',
            'tiers',
            'unit_amount',
            'transform_quantity',
            'metadata',
            'currency_id',
            'lookup_key',
            'currency_options',
            'upsell',
            'pricing_type',
            'base_currency',
            'base_amount',
            'dynamic_pricing_config',
            ...quantityKeys,
          ] // prettier-ignore
    )
  );

  if (updates.lookup_key) {
    const exist = await Price.findOne({ where: { lookup_key: updates.lookup_key } });
    if (exist && exist.id !== doc.id) {
      return res.status(400).json({ error: `lookup_key ${updates.lookup_key} already used by ${exist.id}` });
    }
  }

  if (product.type === 'credit' && updates.metadata) {
    // Merge with existing credit_config if not provided
    const creditConfig = updates.metadata.credit_config || doc.metadata?.credit_config;
    if (!creditConfig) {
      return res.status(400).json({ error: 'credit_config is required' });
    }
    const { error: creditConfigError, value: creditConfigValue } = CreditConfigSchema.validate(creditConfig);
    if (creditConfigError) {
      return res.status(400).json({ error: `credit_config is invalid: ${creditConfigError.message}` });
    }
    updates.metadata.credit_config = creditConfigValue;
  }

  const currencies = await PaymentCurrency.findAll({ where: { active: true } });
  const currency =
    currencies.find((x) => x.id === updates?.currency_id || '') || currencies.find((x) => x.id === doc.currency_id);
  if (!currency) {
    return res
      .status(400)
      .json({ error: `currency used in price not found or not active: ${updates?.currency_id || doc.currency_id}` });
  }
  if (updates.unit_amount) {
    updates.unit_amount = fromTokenToUnit(updates.unit_amount, currency.decimal).toString();
    if (updates.currency_options) {
      const exist = updates.currency_options.find((x) => x.currency_id === currency.id);
      if (exist) {
        exist.unit_amount = fromUnitToToken(updates.unit_amount as string, currency.decimal);
      }
    }
  }
  if (updates.currency_options) {
    updates.currency_options = Price.formatCurrencies(updates.currency_options, currencies);
    const index = updates.currency_options.findIndex((x) => x.currency_id === currency.id);
    if (index > -1) {
      updates.unit_amount = updates.currency_options[index]?.unit_amount;
    } else {
      updates.currency_options.unshift({
        currency_id: currency.id,
        unit_amount: doc.unit_amount,
        tiers: null,
        custom_unit_amount: null,
      });
    }
  }
  const isRecurring = updates.type === 'recurring';
  const { notSupportCurrencies, validate } = await checkCurrencySupportRecurring(
    (updates.currency_options || [])?.map((x) => x.currency_id).filter(Boolean),
    isRecurring
  );
  if (!validate) {
    return res
      .status(400)
      .json({ error: `currency ${notSupportCurrencies.map((x) => x.name).join(', ')} does not support recurring` });
  }

  const pricingType = updates.pricing_type || doc.pricing_type;
  if (pricingType === 'dynamic') {
    const currencyOptions = updates.currency_options || doc.currency_options || [];
    const currencyIds = currencyOptions.map((x: any) => x.currency_id).filter(Boolean);
    const validationError = await validateDynamicPricingCurrencies(
      currencyIds.length ? currencyIds : [updates.currency_id || doc.currency_id]
    );
    if (validationError) {
      return res.status(400).json({ error: validationError });
    }
  }

  try {
    await doc.update(Price.formatBeforeSave(updates));
    logger.info(`Price updated: ${req.params.id}`, { priceId: req.params.id, updates, requestedBy: req.user?.did });
    return res.json(await getExpandedPrice(req.params.id as string));
  } catch (err) {
    logger.error('Error updating price', { error: err.message, request: req.body });
    return res.status(400).json({ error: err.message });
  }
});

// archive
router.put('/:id/archive', auth, async (req, res) => {
  const price = await Price.findByPkOrLookupKey(req.params.id as string);

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

  if (price.active === false) {
    return res.status(403).json({ error: 'price already archived' });
  }

  if (price.locked) {
    return res.status(403).json({ error: 'price locked' });
  }

  await price.update({ active: false });

  logger.info(`Price archived: ${req.params.id}`, { priceId: req.params.id, requestedBy: req.user?.did });
  return res.json(await getExpandedPrice(req.params.id as string));
});

// delete price
router.delete('/:id', auth, async (req, res) => {
  try {
    const price = await Price.findByPkOrLookupKey(req.params.id as string);

    if (!price) {
      return res.status(404).json({ error: 'Can not delete none existing price' });
    }

    if (price.locked) {
      return res.status(403).json({ error: 'Can not delete locked price' });
    }

    if (await price.isUsed(true)) {
      return res.status(403).json({ error: 'Can not delete price used by other resources' });
    }

    await price.destroy();
    return res.json(price);
  } catch (err) {
    logger.error('delete price error', err);
    return res.status(400).json({ error: err.message });
  }
});

const priceInventorySchema = Joi.object({
  quantity: Joi.number().integer().min(0).required(),
  action: Joi.string().valid('decrement', 'increment').required(),
});
router.put('/:id/inventory', auth, async (req, res) => {
  try {
    const { error } = priceInventorySchema.validate(req.body);
    if (error) {
      return res.status(400).json({ error: `Price inventory update request invalid: ${error.message}` });
    }
    const price = await Price.findByPkOrLookupKey(req.params.id as string);
    if (!price) {
      return res.status(404).json({ error: 'price not found' });
    }
    const quantity = Number(req.body.quantity);
    const limitPerCheckQuantity = price.quantity_limit_per_checkout;
    if (limitPerCheckQuantity > 0 && quantity > limitPerCheckQuantity) {
      return res
        .status(400)
        .json({ error: `quantity ${quantity} exceeds limit per checkout ${limitPerCheckQuantity}` });
    }

    if (req.body.action === 'decrement') {
      if (quantity > price.quantity_sold) {
        return res.status(400).json({ error: 'decrementing quantity exceeds sold quantity' });
      }
      await price.decrement('quantity_sold', { by: req.body.quantity });
    }
    if (req.body.action === 'increment') {
      if (price.quantity_available > 0 && price.quantity_sold + quantity > price.quantity_available) {
        return res.status(400).json({ error: 'incrementing sold quantity exceeds available quantity' });
      }
      await price.increment('quantity_sold', { by: req.body.quantity });
    }
    logger.info(`Price inventory updated: ${req.params.id}`, {
      priceId: req.params.id,
      action: req.body.action,
      quantity: req.body.quantity,
      requestedBy: req.user?.did,
    });
    return res.json(await getExpandedPrice(req.params.id as string));
  } catch (err) {
    logger.error('update price inventory error', err);
    return res.status(400).json({ error: err.message });
  }
});

export default router;
