import { isValid } from '@arcblock/did';
import { Router } from 'express';
import Joi from 'joi';
import pick from 'lodash/pick';
import { Op } from 'sequelize';

import { BN, fromTokenToUnit } from '@ocap/util';
import { syncStripeInvoice } from '../integrations/stripe/handlers/invoice';
import { syncStripePayment } from '../integrations/stripe/handlers/payment-intent';
import {
  BNPositiveValidator,
  createListParamSchema,
  getOrder,
  getWhereFromKvQuery,
  getWhereFromQuery,
  MetadataSchema,
} from '../libs/api';
import { authenticate } from '../libs/security';
import { formatMetadata } from '../libs/util';
import { paymentQueue } from '../queues/payment';
import { CheckoutSession } from '../store/models/checkout-session';
import { Customer } from '../store/models/customer';
import { Invoice } from '../store/models/invoice';
import { PaymentCurrency } from '../store/models/payment-currency';
import { PaymentIntent } from '../store/models/payment-intent';
import { PaymentMethod } from '../store/models/payment-method';
import { Subscription } from '../store/models/subscription';
import logger from '../libs/logger';
import { Payout, Refund } from '../store/models';
import { getRefundAmountSetup } from '../libs/refund';

const router = Router();
const authAdmin = authenticate<PaymentIntent>({ component: true, roles: ['owner', 'admin'] });
const authMine = authenticate<PaymentIntent>({ component: true, roles: ['owner', 'admin'], mine: true });
const authPortal = authenticate<PaymentIntent>({
  component: true,
  roles: ['owner', 'admin'],
  record: {
    // @ts-ignore
    model: PaymentIntent,
    field: 'customer_id',
  },
});

async function syncStripePaymentAndInvoice(paymentIntent: PaymentIntent, invoice: Invoice | null, req: any) {
  const shouldSync = paymentIntent.status !== 'succeeded' || req.query.sync === '1';
  if (shouldSync) {
    await syncStripePayment(paymentIntent);
  }
  if (invoice?.metadata?.stripe_id) {
    await syncStripeInvoice(invoice);
  }
}

// list payment intents
const paginationSchema = createListParamSchema<{
  status?: string;
  invoice_id?: string;
  customer_id?: string;
  customer_did?: string;
  currency_id?: string;
}>({
  status: Joi.string().empty(''),
  invoice_id: Joi.string().empty(''),
  customer_id: Joi.string().empty(''),
  customer_did: Joi.string().empty(''),
  currency_id: Joi.string().empty(''),
});
router.get('/', authMine, async (req, res) => {
  const { page, pageSize, status, livemode, ...query } = await paginationSchema.validateAsync(req.query, {
    stripUnknown: false,
    allowUnknown: true,
  });
  const where = getWhereFromKvQuery(query.q);

  if (status) {
    where.status = status
      .split(',')
      .map((x) => x.trim())
      .filter(Boolean);
  }
  if (query.customer_id) {
    where.customer_id = query.customer_id;
  }
  if (query.customer_did && isValid(query.customer_did)) {
    const customer = await Customer.findOne({ where: { did: query.customer_did } });
    if (customer) {
      where.customer_id = customer.id;
    } else {
      res.json({ count: 0, list: [] });
      return;
    }
  }
  if (query.invoice_id) {
    where.invoice_id = query.invoice_id;
  }

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

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

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

  try {
    const { rows: list, count } = await PaymentIntent.findAndCountAll({
      where,
      order: getOrder(req.query, [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']]),
      offset: (page - 1) * pageSize,
      limit: pageSize,
      include: [
        { model: PaymentCurrency, as: 'paymentCurrency' },
        { model: PaymentMethod, as: 'paymentMethod' },
        { model: Customer, as: 'customer' },
      ],
    });

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

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

  const where = q != null ? getWhereFromKvQuery(q) : getWhereFromQuery(query);
  if (typeof livemode === 'boolean') {
    where.livemode = livemode;
  }

  const { rows: list, count } = await PaymentIntent.findAndCountAll({
    where,
    order: getOrder(req.query, [['created_at', o === 'asc' ? 'ASC' : 'DESC']]),
    offset: (page - 1) * pageSize,
    limit: pageSize,
    include: [
      { model: PaymentCurrency, as: 'paymentCurrency' },
      { model: PaymentMethod, as: 'paymentMethod' },
      { model: Customer, as: 'customer' },
    ],
  });

  res.json({ count, list, paging: { page, pageSize } });
});

router.get('/:id', authPortal, async (req, res) => {
  try {
    const doc = await PaymentIntent.findOne({
      where: { id: req.params.id },
      include: [
        { model: PaymentCurrency, as: 'paymentCurrency' },
        { model: PaymentMethod, as: 'paymentMethod' },
        { model: Customer, as: 'customer' },
      ],
    });

    let checkoutSession;
    let invoice;
    let subscription;

    if (doc) {
      invoice = await Invoice.findByPk(doc.invoice_id);
      await syncStripePaymentAndInvoice(doc, invoice, req);

      checkoutSession = await CheckoutSession.findOne({ where: { payment_intent_id: doc.id } });
      if (invoice && invoice.subscription_id) {
        subscription = await Subscription.findByPk(invoice.subscription_id);
      }
    }

    if (doc) {
      res.json({ ...doc.toJSON(), checkoutSession, invoice, subscription });
    } else {
      res.status(404).json(null);
    }
  } catch (err) {
    logger.error(err);
    res.status(500).json({ error: `Failed to get payment intent: ${err.message}` });
  }
});

// eslint-disable-next-line consistent-return
router.put('/:id', authAdmin, async (req, res) => {
  try {
    const doc = await PaymentIntent.findByPk(req.params.id as string);
    if (!doc) {
      return res.status(404).json({ error: 'PaymentIntent not found' });
    }

    const raw = pick(req.body, ['metadata']);
    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);
    }

    await doc.update(raw);
    res.json(doc);
  } catch (err) {
    logger.error(err);
    res.json(null);
  }
});

// eslint-disable-next-line consistent-return
router.get('/:id/retry', authAdmin, async (req, res) => {
  try {
    const doc = await PaymentIntent.findByPk(req.params.id as string);
    if (!doc) {
      return res.status(404).json({ error: 'PaymentIntent not found' });
    }
    if (doc.status === 'succeeded') {
      return res.status(403).json({ error: 'PaymentIntent already done' });
    }
    if (doc.status === 'processing') {
      return res.status(403).json({ error: 'PaymentIntent already processing' });
    }

    await doc.update({ status: 'requires_capture', last_payment_error: null });
    await paymentQueue.pushAndWait({
      id: `${doc.id}-retry-${Date.now()}`,
      job: { paymentIntentId: doc.id, retryOnError: false },
    });
    res.json(doc);
  } catch (err) {
    logger.error(err);
    res.json(null);
  }
});

const refundRequestSchema = Joi.object({
  amount: BNPositiveValidator.required(),
  reason: Joi.string()
    .valid('duplicate', 'requested_by_customer', 'requested_by_admin', 'fraudulent', 'expired_uncaptured_charge')
    .required(),
  description: Joi.string().max(200).required(),
  metadata: Joi.object().optional(),
});

// eslint-disable-next-line consistent-return
router.put('/:id/refund', authAdmin, async (req, res) => {
  try {
    const { error } = refundRequestSchema.validate(req.body);
    if (error) {
      return res.status(400).json({ error: `payment intent refund request invalid: ${error.message}` });
    }
    const doc = await PaymentIntent.findOne({
      where: { id: req.params.id },
      include: [
        { model: PaymentCurrency, as: 'paymentCurrency' },
        { model: PaymentMethod, as: 'paymentMethod' },
      ],
    });
    if (!doc) {
      throw new Error('PaymentIntent not found');
    }

    // @ts-ignore
    if (doc.payment_details?.arcblock?.type === 'slash' && doc.paymentMethod?.type === 'arcblock') {
      throw new Error('PaymentIntent refund not supported for slash payment');
    }

    const invoice = await Invoice.findByPk(doc.invoice_id);
    await syncStripePaymentAndInvoice(doc, invoice, req);
    // @ts-ignore
    const result = await getRefundAmountSetup({
      paymentIntentId: doc.id,
      currencyId: doc.currency_id,
      customerId: doc.customer_id,
      subscriptionId: invoice?.subscription_id,
    });
    if (!result) {
      throw new Error('refund amount setup failed');
    }
    // @ts-ignore
    const amount = fromTokenToUnit(req.body.amount, doc.paymentCurrency?.decimal).toString();
    const amountBN = new BN(amount);

    if (amountBN.gt(new BN(result.amount))) {
      throw new Error('refund amount exceeds the available amount');
    }

    const item = await Refund.create({
      type: 'refund',
      livemode: doc.livemode,
      amount,
      description: req.body.description || 'payment_intent_cancel',
      status: 'pending',
      reason: req.body.reason || 'requested_by_admin',
      currency_id: doc.currency_id,
      customer_id: doc.customer_id || '',
      payment_method_id: doc.payment_method_id,
      payment_intent_id: doc.id,
      invoice_id: doc.invoice_id,
      subscription_id: invoice?.subscription_id,
      attempt_count: 0,
      attempted: false,
      next_attempt: 0,
      last_attempt_error: null,
      starting_balance: '0',
      ending_balance: '0',
      starting_token_balance: {},
      ending_token_balance: {},
      metadata: {
        requested_by: req.user?.did,
      },
    });
    logger.info('payment intent refund created', {
      ...req.params,
      ...req.body,
      ...pick(result, ['amount', 'totalAmount']),
      item: item.toJSON(),
    });
    res.json(item.toJSON());
  } catch (err) {
    logger.error(err);
    res.status(500).json({ error: `Failed to payment intent refund: ${err.message}` });
  }
});

router.get('/:id/refundable-amount', authPortal, async (req, res) => {
  try {
    const doc = await PaymentIntent.findOne({
      where: { id: req.params.id },
    });

    if (doc) {
      const invoice = await Invoice.findByPk(doc.invoice_id);
      await syncStripePaymentAndInvoice(doc, invoice, req);
      const result = await getRefundAmountSetup({
        paymentIntentId: doc.id,
        currencyId: doc.currency_id,
        customerId: doc.customer_id,
        subscriptionId: invoice?.subscription_id,
      });
      const payouts = await Payout.findAll({
        where: {
          payment_intent_id: doc.id,
          status: {
            [Op.notIn]: ['canceled', 'reverted'],
          },
        },
        attributes: ['id', 'amount'],
      });
      if (payouts.length > 0) {
        let totalPayoutAmount = new BN('0');
        payouts.forEach((payout) => {
          totalPayoutAmount = totalPayoutAmount.add(new BN(payout.amount || '0'));
        });

        result.amount = new BN(result.amount || '0').sub(totalPayoutAmount).toString();
      }
      res.json(result);
    } else {
      res.status(404).json(null);
    }
  } catch (err) {
    logger.error(err);
    res.status(500).json({ error: `Failed to get payment intent refundable amount: ${err.message}` });
  }
});

export default router;
