import { isValid } from '@arcblock/did';
import { getUrl } from '@blocklet/sdk/lib/component';
import { Router } from 'express';
import Joi from 'joi';
import merge from 'lodash/merge';
import pick from 'lodash/pick';
import uniq from 'lodash/uniq';
import type { WhereOptions } from 'sequelize';

import { checkPassportForPricingTable } from '../integrations/blocklet/passport';
import { createListParamSchema, getOrder, MetadataSchema } from '../libs/api';
import logger from '../libs/logger';
import { authenticate } from '../libs/security';
import { getBillingThreshold, getMinStakeAmount, isCreditMetered, isLineItemCurrencyAligned } from '../libs/session';
import { getDaysUntilCancel, getDaysUntilDue } from '../libs/subscription';
import { getDataObjectFromQuery } from '../libs/util';
import { CheckoutSession } from '../store/models/checkout-session';
import { PaymentCurrency } from '../store/models/payment-currency';
import { Price } from '../store/models/price';
import { PricingTable } from '../store/models/pricing-table';
import { Product } from '../store/models/product';
import { formatCheckoutSession } from './checkout-sessions';

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

const PricingTableCreateSchema = Joi.object({
  name: Joi.string().required(),
  description: Joi.string().allow(null, ''),
  metadata: MetadataSchema,
  items: Joi.array()
    .items(
      Joi.object({
        price_id: Joi.string().required(),
        product_id: Joi.string().required(),
        after_completion: Joi.object({
          type: Joi.string().valid('hosted_confirmation', 'redirect').required(),
          hosted_confirmation: Joi.object({
            custom_message: Joi.string().max(200).empty('').optional(),
          })
            .min(0)
            .optional(),
          redirect: Joi.object({
            url: Joi.string().uri().max(2048).empty('').optional(),
          })
            .min(0)
            .optional(),
        }).unknown(true),
        nft_mint_settings: Joi.object({
          enabled: Joi.boolean().required(),
          factory: Joi.string().max(40).empty('').optional(),
        })
          .min(0)
          .optional(),
      }).unknown(true)
    )
    .min(1)
    .required(),
  branding_settings: Joi.object().allow(null, {}).optional(),
}).unknown(true);
// eslint-disable-next-line consistent-return
router.post('/', auth, async (req, res) => {
  const { error } = PricingTableCreateSchema.validate(req.body);
  if (error) {
    return res.status(400).json({ error: `Pricing table create request invalid: ${error.message}` });
  }
  const raw: Partial<PricingTable> = PricingTable.format(req.body);
  raw.active = true;
  raw.locked = false;
  raw.livemode = !!req.livemode;
  raw.created_via = req.user?.via;

  if (!raw.items?.length) {
    return res.status(400).json({ error: 'items should not be empty for pricing table' });
  }

  // @ts-ignore
  const items = await Price.expand(raw.items);
  for (let i = 0; i < items.length; i++) {
    if (isLineItemCurrencyAligned(items, i) === false) {
      return res.status(400).json({ error: 'items should have same currency' });
    }
  }

  const link = await PricingTable.create(raw as PricingTable);

  res.json(link);
});

// list pricing tables
const paginationSchema = createListParamSchema<{
  active?: boolean;
}>({
  active: Joi.boolean().empty(''),
});
router.get('/', auth, async (req, res) => {
  const { page, pageSize, ...query } = await paginationSchema.validateAsync(req.query, { stripUnknown: true });
  const where: WhereOptions<PricingTable> = { 'metadata.preview': null };

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

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

    const priceIds: string[] = uniq(list.reduce((acc: string[], x) => acc.concat(x.items.map((i) => i.price_id)), []));
    const prices = await Price.findAll({ where: { id: priceIds }, include: [{ model: Product, as: 'product' }] });
    // Derive products from the already-included price.product association (avoids redundant Product.findAll)
    const productMap = new Map(prices.filter((p) => (p as any).product).map((p) => [p.product_id, (p as any).product]));

    list.forEach((x) => {
      x.items.forEach((i) => {
        // @ts-ignore
        i.price = prices.find((p) => p.id === i.price_id);
        // @ts-ignore
        i.product = productMap.get(i.product_id);
      });
    });

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

// eslint-disable-next-line consistent-return
router.get('/:id', async (req, res) => {
  const doc = await PricingTable.findByPk(req.params.id);

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

  let currency = await PaymentCurrency.findOne({ where: { livemode: doc.livemode, is_base_currency: true } });
  const expanded = await doc.expand();
  if (expanded.items.length > 0) {
    const index = expanded.items.findIndex((x: any) => x.price?.currency_id === currency?.id);
    if (index === -1) {
      currency = await PaymentCurrency.findOne({
        where: {
          livemode: doc.livemode,
          // @ts-ignore
          id: expanded?.items?.[0]?.price?.currency_id,
        },
      });
    }
  }
  res.json({ ...expanded, currency });
});

// update
// 更新schema，局部更新
const PricingTableUpdateSchema = Joi.object({
  name: Joi.string().optional(),
  description: Joi.string().allow(null, '').optional(),
  metadata: MetadataSchema.optional(),
  items: Joi.array()
    .items(
      Joi.object({
        price_id: Joi.string().required(),
        product_id: Joi.string().required(),
        after_completion: Joi.object({
          type: Joi.string().valid('hosted_confirmation', 'redirect').required(),
          hosted_confirmation: Joi.object({
            custom_message: Joi.string().max(200).empty('').optional(),
          })
            .min(0)
            .optional(),
          redirect: Joi.object({
            url: Joi.string().uri().max(2048).empty('').optional(),
          })
            .min(0)
            .optional(),
        }).unknown(true),
        nft_mint_settings: Joi.object({
          enabled: Joi.boolean().required(),
          factory: Joi.string().max(40).empty('').optional(),
        })
          .min(0)
          .optional(),
      }).unknown(true)
    )
    .min(1)
    .optional(),
  branding_settings: Joi.object().allow(null, {}).optional(),
}).unknown(true);

// eslint-disable-next-line consistent-return
router.put('/:id', auth, async (req, res) => {
  const { error } = PricingTableUpdateSchema.validate(req.body);
  if (error) {
    return res.status(400).json({ error: `Pricing table create request invalid: ${error.message}` });
  }
  const doc = await PricingTable.findByPk(req.params.id);

  if (!doc) {
    return res.status(404).json({ error: 'pricing table not found' });
  }
  if (doc.active === false) {
    return res.status(403).json({ error: 'pricing table archived' });
  }
  // if (doc.locked) {
  //   return res.status(403).json({ error: 'pricing table locked' });
  // }

  await doc.update(PricingTable.format(Object.assign({}, doc.dataValues, req.body)));

  res.json(doc);
});

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

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

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

  await doc.update({ active: false });
  return res.json(doc);
});

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

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

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

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

  await doc.destroy();
  return res.json(doc);
});

router.post('/stash', auth, async (req, res) => {
  try {
    const raw: Partial<PricingTable> = req.body;
    raw.id = `prctbl_${req.user?.did}`;
    raw.active = true;
    raw.locked = false;
    raw.livemode = !!req.livemode;
    raw.created_via = req.user?.via;
    raw.metadata = { preview: '1' };

    let doc = await PricingTable.findByPk(raw.id);
    if (doc) {
      await doc.update({ ...PricingTable.format(req.body), metadata: raw.metadata, livemode: raw.livemode });
    } else {
      doc = await PricingTable.create(raw as PricingTable);
    }
    res.json(doc);
  } catch (err) {
    logger.error(err);
    res.status(500).json({ error: err.message });
  }
});

// start checkout session for pricing table
// eslint-disable-next-line consistent-return
router.post('/:id/checkout/:priceId', async (req, res) => {
  const doc = await PricingTable.findByPk(req.params.id);

  if (!doc) {
    return res.status(404).json({ error: 'pricing table not found' });
  }
  if (doc.active === false) {
    return res.status(403).json({ error: 'pricing table archived' });
  }
  if (doc.locked) {
    return res.status(403).json({ error: 'pricing table locked' });
  }

  const lineItem = await doc.items.find((x) => x.price_id === req.params.priceId);
  if (!lineItem) {
    return res.status(403).json({ error: 'pricing table item not valid' });
  }

  const price = await Price.findByPk(lineItem.price_id);
  if (!price) {
    return res.status(403).json({ error: 'price not found' });
  }

  const raw: Partial<CheckoutSession> = await formatCheckoutSession({
    line_items: [{ price_id: lineItem.price_id, quantity: 1, adjustable_quantity: lineItem.adjustable_quantity }],
    ...pick(price, [
      'allow_promotion_codes',
      'consent_collection',
      'custom_fields',
      'customer_creation',
      'invoice_creation',
      'phone_number_collection',
      'billing_address_collection',
      'submit_type',
      'cross_sell_behavior',
      'nft_mint_settings',
    ]),
    subscription_data: merge(lineItem.subscription_data || {}, getDataObjectFromQuery(req.query, 'subscription_data'), {
      billing_threshold_amount: getBillingThreshold(lineItem.subscription_data),
      min_stake_amount: getMinStakeAmount(lineItem.subscription_data),
      no_stake: isCreditMetered(price),
    }),
    metadata: {
      ...doc.metadata,
      ...getDataObjectFromQuery(req.query),
      days_until_due: getDaysUntilDue(req.query),
      days_until_cancel: getDaysUntilCancel(req.query),
      passport: await checkPassportForPricingTable(doc),
      pricing_table_id: doc.id,
    },
  });

  const currency = await PaymentCurrency.findOne({ where: { livemode: doc.livemode, is_base_currency: true } });

  raw.livemode = doc.livemode;
  raw.created_via = 'portal';
  raw.currency_id = (req.query?.currencyId as string) ?? currency?.id;

  if (req.query.redirect) {
    raw.success_url = req.query.redirect as string;
    raw.cancel_url = req.query.redirect as string;
    logger.info('use redirect from query when checkout from pricing table', { v: raw.success_url });
  }
  if (req.query.cross_sell_behavior) {
    raw.cross_sell_behavior = req.query.cross_sell_behavior as string;
    logger.info('use cross_sell_behavior from query when checkout from pricing table', { v: raw.cross_sell_behavior });
  }
  try {
    if (req.query.nft_mint_factory && isValid(req.query.nft_mint_factory as string)) {
      raw.nft_mint_settings = {
        enabled: true,
        behavior: 'per_checkout_session',
        factory: req.query.nft_mint_factory as string,
      };
      raw.nft_mint_status = 'pending';
      logger.info('use nft_mint_settings from query when checkout from pricing table', { v: raw.nft_mint_settings });
    } else {
      raw.nft_mint_status = 'disabled';
    }
  } catch {
    // Do nothing
  }

  const session = await CheckoutSession.create(raw as any);
  res.json({ ...session.toJSON(), url: getUrl(`/checkout/pay/${session.id}`) });
});

export default router;
