import { Joi } from '@arcblock/validator';
import { Router } from 'express';
import pick from 'lodash/pick';
import { Op } from 'sequelize';
import type { WhereOptions } from 'sequelize';

import { createListParamSchema, getOrder, MetadataSchema } from '../libs/api';
import logger from '../libs/logger';
import { authenticate } from '../libs/security';
import { isLineItemAligned } from '../libs/session';
import { formatMetadata } from '../libs/util';
import { PaymentLink } from '../store/models/payment-link';
import { Price } from '../store/models/price';
import { Product } from '../store/models/product';
import { getDonationBenefits } from '../libs/payment';

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

const formatBeforeSave = (payload: any) => {
  const raw: Partial<PaymentLink> = Object.assign(
    {
      after_completion: {
        type: 'hosted_confirmation',
        hosted_confirmation: {
          custom_message: '',
        },
      },
      allow_promotion_codes: false,
      customer_creation: 'always',
      consent_collection: {
        promotions: 'none',
        terms_of_service: 'none',
      },
      invoice_creation: {
        enabled: true,
      },
      phone_number_collection: {
        enabled: false,
      },
      billing_address_collection: 'auto',
      subscription_data: {
        description: '',
        trial_period_days: 0,
      },
      nft_mint_settings: {
        enabled: false,
        factory: '',
      },
      submit_type: 'pay',
      cross_sell_behavior: 'auto',
      payment_intent_data: null,
      donation_settings: null,
      enable_subscription_grouping: false,
      lookup_key: null,
    },
    pick(payload, [
      'name',
      'line_items',
      'currency_id',
      'after_completion',
      'allow_promotion_codes',
      'consent_collection',
      'custom_fields',
      'customer_creation',
      'invoice_creation',
      'phone_number_collection',
      'billing_address_collection',
      'submit_type',
      'subscription_data',
      'payment_intent_data',
      'nft_mint_settings',
      'cross_sell_behavior',
      'donation_settings',
      'metadata',
      'enable_subscription_grouping',
      'lookup_key',
    ])
  );

  // TODO: need to support stake subscription
  if (raw.enable_subscription_grouping === true && !raw.subscription_data?.no_stake) {
    throw new Error('Subscription grouping is only supported for stake-free subscriptions');
  }

  if (raw.after_completion?.type === 'hosted_confirmation') {
    // @ts-ignore
    raw.after_completion.redirect = null;
  }
  if (raw.after_completion?.type === 'redirect') {
    // @ts-ignore
    raw.after_completion.hosted_confirmation = null;
  }
  if (typeof payload.include_free_trial === 'boolean' && !payload.include_free_trial) {
    // @ts-ignore
    raw.subscription_data.trial_period_days = 0;
  }

  if (raw.nft_mint_settings?.enabled) {
    if (!raw.nft_mint_settings?.factory) {
      throw new Error('factory is required when nft mint is enabled');
    }
  }

  raw.line_items?.forEach((x) => {
    if (x.adjustable_quantity?.enabled) {
      if (Number(x.adjustable_quantity?.minimum) >= Number(x.adjustable_quantity?.maximum)) {
        throw new Error('adjustable_quantity.minimum must be less than adjustable_quantity.maximum');
      }
      x.adjustable_quantity.minimum = Number(x.adjustable_quantity?.minimum);
      x.adjustable_quantity.maximum = Number(x.adjustable_quantity?.maximum);
    }
  });

  raw.metadata = formatMetadata(raw.metadata);

  return raw;
};

export async function createPaymentLink(payload: any) {
  const raw: Partial<PaymentLink> = formatBeforeSave(payload);
  raw.active = true;
  raw.livemode = !!payload.livemode;
  raw.created_via = payload.via;
  raw.currency_id = payload.currency_id;

  if (!raw.line_items?.length) {
    throw new Error('line_items should not be empty for payment link');
  }

  const invalidQuantityIndex = raw.line_items.findIndex((x) => Number(x?.quantity || 0) < 1);
  if (invalidQuantityIndex > -1) {
    throw new Error(`line_items[${invalidQuantityIndex}].quantity should be greater or equal to 1`);
  }

  const items = await Price.expand(raw.line_items);
  if (items.find((x) => x.price?.custom_unit_amount) && items.length > 1) {
    throw new Error('Multiple items with custom unit amount are not supported in payment link');
  }

  // Check for mixed pricing types (fixed + dynamic)
  const hasDynamicPricing = items.some((item) => {
    return item.price?.pricing_type === 'dynamic';
  });

  const hasFixedPricing = items.some((item) => {
    return !item.price?.pricing_type || item.price?.pricing_type === 'fixed';
  });

  if (hasDynamicPricing && hasFixedPricing) {
    throw new Error('Cannot mix fixed pricing and dynamic pricing in the same payment link');
  }

  for (let i = 0; i < items.length; i++) {
    const result = isLineItemAligned(items, i);
    if (result.currency === false) {
      throw new Error('line_items should have same currency');
    }
    if (result.recurring === false) {
      throw new Error('line_items should have same recurring');
    }
  }

  if (items.some((x) => x.price.type === 'recurring')) {
    raw.invoice_creation = { enabled: true };
  }

  return PaymentLink.insert(raw as PaymentLink);
}

const PaymentLinkCreateSchema = Joi.object({
  name: Joi.string().max(255).empty('').optional(),
  lookup_key: Joi.string().max(128).empty('').optional(),
  line_items: Joi.array()
    .items(
      Joi.object({
        price_id: Joi.string().required(),
        quantity: Joi.number().min(1).required(),
        adjustable_quantity: Joi.object({
          enabled: Joi.boolean().required(),
          minimum: Joi.number().min(0),
          maximum: Joi.number().min(0),
        })
          .min(0)
          .optional(),
      })
    )
    .min(1)
    .required(),
  currency_id: Joi.string().optional(),
  metadata: MetadataSchema,
  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(),
  })
    .min(0)
    .optional(),
  allow_promotion_codes: Joi.boolean().optional(),
  enable_subscription_grouping: Joi.boolean().optional(),
  nft_mint_settings: Joi.object({
    enabled: Joi.boolean().required(),
    factory: Joi.string().max(40).empty('').optional(),
  })
    .min(0)
    .optional(),
  consent_collection: Joi.object({
    promotions: Joi.string().valid('none', 'opt_in', 'opt_out').required(),
    terms_of_service: Joi.string().valid('none', 'opt_in', 'opt_out').required(),
  })
    .min(0)
    .optional(),
}).unknown(true);
router.post('/', auth, async (req, res) => {
  try {
    const { error } = PaymentLinkCreateSchema.validate(req.body);
    if (error) {
      logger.warn('Payment link create request invalid', { error: error.message, body: req.body });
      res.status(400).json({ error: `Payment link create request invalid: ${error.message}` });
      return;
    }
    const result = await createPaymentLink({
      ...req.body,
      livemode: !!req.livemode,
      created_via: req.user?.via,
      currency_id: req.body.currency_id || req.baseCurrency.id,
      metadata: formatMetadata(req.body.metadata),
    });
    logger.info('Payment link created successfully', { id: result.id, user: req.user?.did });
    res.json(result);
  } catch (err) {
    logger.error('Create payment link error', { error: err.message, stack: err.stack, body: req.body });
    res.status(400).json({ error: err.message });
  }
});

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

  if (typeof query.active === 'boolean') {
    where.active = query.active;
  }
  if (typeof query.livemode === 'boolean') {
    where.livemode = query.livemode;
  }
  if (query.donation === 'hide') {
    where.submit_type = { [Op.not]: 'donate' };
  }

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

    const priceIds: string[] = list.reduce((acc: string[], x) => acc.concat(x.line_items.map((i) => i.price_id)), []);
    const prices = await Price.findAll({ where: { id: priceIds }, include: [{ model: Product, as: 'product' }] });

    const priceMap = new Map(prices.map((p) => [p.id, p]));

    list.forEach((x) => {
      x.line_items.forEach((i) => {
        // @ts-ignore
        i.price = priceMap.get(i.price_id);
      });
    });

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

router.get('/:id', auth, async (req, res) => {
  const doc = await PaymentLink.findByPkOrLookupKey(req.params.id || '');

  if (doc) {
    // @ts-ignore
    doc.line_items = await Price.expand(doc.line_items);
  }

  if (doc) {
    res.json(doc);
  } else {
    res.status(404).json(null);
  }
});

const PaymentLinkUpdateSchema = Joi.object({
  name: Joi.string().optional(),
  line_items: Joi.array()
    .items(
      Joi.object({
        price_id: Joi.string().required(),
        quantity: Joi.number().min(1).required(),
        adjustable_quantity: Joi.object({
          enabled: Joi.boolean().required(),
          minimum: Joi.number().min(0),
          maximum: Joi.number().min(0),
        })
          .min(0)
          .optional(),
      })
    )
    .min(1)
    .optional(),
  currency_id: Joi.string().optional(),
  metadata: MetadataSchema,
  active: Joi.boolean().optional(),
  after_completion: Joi.object({
    type: Joi.string().valid('hosted_confirmation', 'redirect').optional(),
    hosted_confirmation: Joi.object({
      custom_message: Joi.string().max(200).empty('').optional(),
    }),
    redirect: Joi.object({
      url: Joi.string().uri().max(2048).empty('').optional(),
    }),
  })
    .min(0)
    .optional(),
  allow_promotion_codes: Joi.boolean().optional(),
  enable_subscription_grouping: Joi.boolean().optional(),
  nft_mint_settings: Joi.object({
    enabled: Joi.boolean().required(),
    factory: Joi.string().max(40).empty('').optional(),
  })
    .min(0)
    .optional(),
  consent_collection: Joi.object({
    promotions: Joi.string().valid('none', 'opt_in', 'opt_out').optional(),
    terms_of_service: Joi.string().valid('none', 'opt_in', 'opt_out').optional(),
  })
    .min(0)
    .optional(),
}).unknown(true);
// eslint-disable-next-line consistent-return
router.put('/:id', auth, async (req, res) => {
  const { error } = PaymentLinkUpdateSchema.validate(req.body);
  if (error) {
    logger.warn('Payment link update request invalid', { error: error.message, id: req.params.id, body: req.body });
    return res.status(400).json({ error: `Payment link update request invalid: ${error.message}` });
  }
  const doc = await PaymentLink.findByPk(req.params.id);

  if (!doc) {
    logger.warn('Payment link not found for update', { id: req.params.id });
    return res.status(404).json({ error: 'payment link not found' });
  }
  if (doc.active === false) {
    logger.warn('Attempt to update archived payment link', { id: req.params.id });
    return res.status(403).json({ error: 'payment link archived' });
  }

  try {
    await doc.update(formatBeforeSave(Object.assign({}, doc.dataValues, req.body)));
    logger.info('Payment link updated successfully', { id: req.params.id, user: req.user?.did });
    res.json(doc);
  } catch (err) {
    logger.error('Update payment link error', {
      error: err.message,
      stack: err.stack,
      id: req.params.id,
      body: req.body,
    });
    res.status(500).json({ error: 'Failed to update payment link' });
  }
});

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

  if (!doc) {
    logger.warn('Payment link not found for archiving', { id: req.params.id });
    return res.status(404).json({ error: 'payment link not found' });
  }

  if (doc.active === false) {
    logger.warn('Attempt to archive already archived payment link', { id: req.params.id });
    return res.status(403).json({ error: 'payment link already archived' });
  }

  try {
    await doc.update({ active: false });
    logger.info('Payment link archived successfully', { id: req.params.id, user: req.user?.did });
    return res.json(doc);
  } catch (err) {
    logger.error('Archive payment link error', { error: err.message, stack: err.stack, id: req.params.id });
    return res.status(500).json({ error: 'Failed to archive payment link' });
  }
});

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

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

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

  try {
    await doc.destroy();
    logger.info('Payment link deleted successfully', { id: req.params.id, user: req.user?.did });
    return res.json(doc);
  } catch (err) {
    logger.error('Delete payment link error', { error: err.message, stack: err.stack, id: req.params.id });
    return res.status(500).json({ error: 'Failed to delete payment link' });
  }
});

router.post('/stash', auth, async (req, res) => {
  try {
    const raw: Partial<PaymentLink> = req.body;
    raw.id = `plink_${req.user?.did}`;
    raw.active = true;
    raw.livemode = !!req.livemode;
    raw.created_via = req.user?.via;
    raw.currency_id = raw.currency_id || req.baseCurrency.id;
    // Merge existing metadata with preview flag
    raw.metadata = { ...raw.metadata, preview: '1' };

    let doc = await PaymentLink.findByPk(raw.id);
    if (doc) {
      await doc.update({ ...formatBeforeSave(req.body), livemode: raw.livemode });
      logger.info('Stashed payment link updated', { id: raw.id, user: req.user?.did });
    } else {
      doc = await PaymentLink.create(raw as PaymentLink);
      logger.info('New stashed payment link created', { id: raw.id, user: req.user?.did });
    }
    res.json(doc);
  } catch (err) {
    logger.error('Stash payment link error', { error: err.message, stack: err.stack, body: req.body });
    res.status(500).json({ error: err.message });
  }
});

router.get('/:id/benefits', async (req, res) => {
  try {
    const doc = await PaymentLink.findByPk(req.params.id, {
      attributes: ['id', 'donation_settings'],
    });
    if (!doc) {
      return res.status(404).json({ error: 'payment link not found' });
    }
    const locale = req.query.locale as string;
    const benefits = await getDonationBenefits(doc, '', locale);
    return res.json(benefits);
  } catch (err) {
    logger.error('Get donation benefits error', { error: err.message, stack: err.stack, id: req.params.id });
    return res.status(400).json({ error: err.message });
  }
});

export default router;
