import { fromTokenToUnit } from '@ocap/util';
import { Router } from 'express';
import { InferAttributes, Op, WhereOptions } from 'sequelize';

import Joi from 'joi';
import pick from 'lodash/pick';
import { sessionMiddleware } from '@blocklet/sdk/lib/middlewares/session';
import { fetchErc20Meta } from '../integrations/ethereum/token';
import logger from '../libs/logger';
import { authenticate } from '../libs/security';
import { PaymentCurrency, TPaymentCurrency } from '../store/models/payment-currency';
import { PaymentMethod } from '../store/models/payment-method';
import { Price, Product } from '../store/models';
import { EVM_CHAIN_TYPES } from '../libs/constants';
import { ethWallet, getVaultAddress, wallet } from '../libs/auth';
import { resolveAddressChainTypes } from '../libs/util';
import { depositVaultQueue } from '../queues/payment';
import { checkDepositVaultAmount } from '../libs/payment';
import { getTokenSummaryByDid } from '../integrations/arcblock/stake';
import { MetadataSchema } from '../libs/api';
import { getRechargePaymentUrl } from '../libs/currency';
import { checkCurrencySupportRecurring } from '../libs/product';

const router = Router();

const user = sessionMiddleware({ accessKey: true });
const auth = authenticate<PaymentCurrency>({ component: true, roles: ['owner', 'admin'] });
const authOwner = authenticate<PaymentCurrency>({ component: true, roles: ['owner'] });
const paymentCurrencyCreateSchema = Joi.object({
  name: Joi.string().max(32).required(),
  description: Joi.string().max(255).required(),
}).unknown(true);
router.post('/', auth, async (req, res) => {
  const raw: Partial<TPaymentCurrency> = req.body;

  const { error } = paymentCurrencyCreateSchema.validate(pick(req.body, ['name', 'description']));
  if (error) {
    return res.status(400).json({ error: error.message });
  }

  if (!raw.payment_method_id) {
    return res.status(400).json({ error: 'payment_method_id is required' });
  }
  if (!raw.name) {
    return res.status(400).json({ error: 'payment currency name is required' });
  }
  if (!raw.description) {
    return res.status(400).json({ error: 'payment currency description is required' });
  }
  const method = await PaymentMethod.findByPk(raw.payment_method_id);
  if (!method) {
    return res.status(400).json({ error: 'payment method not found' });
  }
  raw.logo = raw.logo || method.logo;
  if (!raw.logo) {
    return res.status(400).json({ error: 'payment currency logo is required' });
  }
  if (!raw.contract) {
    return res.status(400).json({ error: 'payment currency contract is required' });
  }
  const exist = await PaymentCurrency.findOne({ where: { contract: raw.contract } });
  if (exist) {
    return res.status(400).json({ error: 'payment currency with same contract already exist' });
  }

  if (method.type === 'stripe') {
    return res.status(400).json({ error: 'Adding method for stripe not supported' });
  }

  if (EVM_CHAIN_TYPES.includes(method.type)) {
    try {
      const client = method.getEvmClient();
      const info = await fetchErc20Meta(client, raw.contract);
      logger.info(`${method.type} erc20 info fetched`, { raw, info });
      if (!info.symbol || !info.decimal) {
        return res.status(400).json({ error: `${method.type} token not found` });
      }

      const currency = await PaymentCurrency.create({
        livemode: method.livemode,
        active: method.active,
        locked: false,
        is_base_currency: false,

        payment_method_id: method.id,
        name: raw.name,
        description: raw.description,
        contract: raw.contract,
        logo: raw.logo,
        symbol: info.symbol,
        decimal: info.decimal,
        type: 'standard',
        maximum_precision: 6,

        // FIXME: make these configurable
        minimum_payment_amount: fromTokenToUnit(0.000001, info.decimal).toString(),
        maximum_payment_amount: fromTokenToUnit(100000000, info.decimal).toString(),

        metadata: {},
      });
      return res.json(currency);
    } catch (err) {
      logger.error(err);
      return res.status(400).json({ error: `${method.type} currency contract verify failed` });
    }
  }

  if (method.type === 'arcblock') {
    try {
      const client = method.getOcapClient();
      const { state } = await client.getTokenState({ address: raw.contract });
      logger.info('ocap token info fetched', { raw, state });
      if (!state) {
        return res.status(400).json({ error: 'ocap token not found' });
      }

      const currency = await PaymentCurrency.create({
        livemode: method.livemode,
        active: method.active,
        locked: false,
        is_base_currency: false,

        payment_method_id: method.id,
        name: raw.name,
        description: raw.description,
        contract: raw.contract,
        logo: raw.logo,
        symbol: state.symbol,
        decimal: state.decimal,
        type: 'standard',
        maximum_precision: 6,

        // FIXME: make these configurable
        minimum_payment_amount: fromTokenToUnit(0.000001, state.decimal).toString(),
        maximum_payment_amount: fromTokenToUnit(100000000, state.decimal).toString(),

        metadata: {},
      });
      return res.json(currency);
    } catch (err) {
      logger.error(err);
      return res.status(400).json({ error: `${method.type} currency contract verify failed` });
    }
  }

  return res.status(400).json({ error: `add currency for payment method ${method.type} is not supported` });
});

router.get('/', auth, async (req, res) => {
  const { query } = req;
  const where: WhereOptions<InferAttributes<PaymentCurrency>> = {};

  if (typeof query.active === 'string') {
    where.active = JSON.parse(query.active);
  }
  if (typeof query.livemode === 'string') {
    where.livemode = JSON.parse(query.livemode);
  }
  where.type = 'standard';
  if (query.credit) {
    where.type = {
      [Op.in]: ['standard', 'credit'],
    };
  }
  const list = await PaymentCurrency.findAll({
    where,
    order: [['created_at', 'DESC']],
    include: [],
  });

  res.json(list);
});

router.get('/vault-config', auth, async (req, res) => {
  const vaultAddress = await getVaultAddress();
  if (!vaultAddress) {
    return res.json({
      list: [],
      balances: {},
    });
  }
  const chainTypes = resolveAddressChainTypes(vaultAddress);
  try {
    const paymentMethods = await PaymentMethod.findAll({
      where: {
        type: {
          [Op.in]: chainTypes,
        },
      },
      attributes: ['id'],
    });

    const paymentMethodIds = paymentMethods.map((method) => method.id);
    const list = await PaymentCurrency.scope('withVaultConfig').findAll({
      where: {
        payment_method_id: {
          [Op.in]: paymentMethodIds,
        },
        livemode: !!req.livemode,
      },
      include: [{ model: PaymentMethod, as: 'payment_method' }],
    });
    try {
      const [arcblock, ethereum] = await Promise.all([
        getTokenSummaryByDid(wallet.address, !!req.livemode, 'arcblock'),
        getTokenSummaryByDid(ethWallet.address, !!req.livemode, EVM_CHAIN_TYPES),
      ]);
      return res.json({
        list,
        balances: {
          ...arcblock,
          ...ethereum,
        },
      });
    } catch (err) {
      logger.error('get token summary failed', err);
      return res.status(400).json({ error: err.message, list, balances: {} });
    }
  } catch (err) {
    logger.error('get payment currency vault config failed', err);
    return res.status(400).json({ error: err.message, list: [], balances: {} });
  }
});

router.get('/:id/deposit-vault', auth, async (req, res) => {
  const { id } = req.params;
  if (!id) {
    return res.status(400).json({ error: 'Missing payment currency id' });
  }
  try {
    const result = await checkDepositVaultAmount(id);
    return res.json(result);
  } catch (error) {
    logger.error('Error checking deposit vault amount', { error, id });
    return res.status(400).json({ error: 'Failed to check deposit vault amount', message: error.message });
  }
});

router.put('/:id/deposit-vault', auth, async (req, res) => {
  const paymentCurrency = await PaymentCurrency.findByPk(req.params.id);
  if (!paymentCurrency) {
    return res.status(404).json({ error: 'Payment currency not found' });
  }
  const vaultAddress = await getVaultAddress();
  if (!vaultAddress) {
    return res.status(400).json({ error: 'Vault address not found' });
  }
  depositVaultQueue.push({
    id: `deposit-vault-${paymentCurrency.id}`,
    job: { currencyId: paymentCurrency.id },
  });
  logger.info('Deposit vault job pushed', { currencyId: paymentCurrency.id });
  return res.json({ message: 'Deposit vault job pushed' });
});

router.get('/:id', auth, async (req, res) => {
  const doc = await PaymentCurrency.findOne({
    where: { [Op.or]: [{ id: req.params.id }, { symbol: req.params.id }] },
  });

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

const UpdateVaultConfigSchema = Joi.object({
  enabled: Joi.boolean().required(),
  deposit_threshold: Joi.number().greater(0).required(),
  withdraw_threshold: Joi.number().min(0).required(),
  buffer_threshold: Joi.number().min(0).required(),
});
router.put('/:id/vault-config', authOwner, async (req, res) => {
  try {
    const { id } = req.params;

    const { error, value: vaultConfig } = UpdateVaultConfigSchema.validate(req.body);
    if (error) {
      return res.status(400).json({ error: error.message });
    }

    const paymentCurrency = await PaymentCurrency.findByPk(id);
    if (!paymentCurrency) {
      return res.status(404).json({ error: 'payment currency not found' });
    }

    const vaultAddress = await getVaultAddress();
    if (!vaultAddress) {
      return res.status(400).json({ error: 'Vault address not found' });
    }

    const updateData: Partial<TPaymentCurrency> = {
      vault_config: {
        enabled: vaultConfig.enabled,
        deposit_threshold: fromTokenToUnit(vaultConfig.deposit_threshold, paymentCurrency.decimal).toString(),
        withdraw_threshold: fromTokenToUnit(vaultConfig.withdraw_threshold, paymentCurrency.decimal).toString(),
        buffer_threshold: fromTokenToUnit(vaultConfig.buffer_threshold || 0, paymentCurrency.decimal).toString(),
      },
    };

    await paymentCurrency.update(updateData);

    return res.json(paymentCurrency.toJSON());
  } catch (err) {
    logger.error('update payment currency vault config failed', err);
    return res.status(400).json({ error: err.message });
  }
});

const updateCurrencySchema = Joi.object({
  name: Joi.string().empty('').max(32).optional(),
  description: Joi.string().empty('').max(255).optional(),
  logo: Joi.string().empty('').optional(),
  metadata: MetadataSchema,
  symbol: Joi.string().empty('').optional(),
}).unknown(true);
router.put('/:id', auth, async (req, res) => {
  const { id } = req.params;
  const raw: Partial<TPaymentCurrency> = req.body;

  const { error } = updateCurrencySchema.validate(raw);
  if (error) {
    return res.status(400).json({ error: error.message });
  }

  const currency = await PaymentCurrency.findByPk(id);
  if (!currency) {
    return res.status(404).json({ error: 'Payment currency not found' });
  }
  if (raw.contract && raw.contract !== currency.contract) {
    return res.status(400).json({ error: 'contract cannot be updated' });
  }

  const method = await PaymentMethod.findByPk(currency.payment_method_id);
  if (!method) {
    return res.status(400).json({ error: 'Payment method not found' });
  }

  const updates: Partial<TPaymentCurrency> = {
    name: raw.name || currency.name,
    description: raw.description || currency.description,
    logo: raw.logo || method.logo,
    metadata: raw.metadata || currency.metadata,
  };

  if (currency.type === 'credit') {
    updates.symbol = raw.symbol || currency.symbol;
  }

  const updatedCurrency = await currency.update(updates);
  return res.json(updatedCurrency);
});

const tokenConfigSchema = Joi.object({
  token_factory_address: Joi.string().required(),
});

router.put('/:id/token-config', auth, async (req, res) => {
  try {
    const { id } = req.params;

    const { error, value } = tokenConfigSchema.validate(req.body);
    if (error) {
      return res.status(400).json({ error: error.message });
    }

    const currency = await PaymentCurrency.findByPk(id);
    if (!currency) {
      return res.status(404).json({ error: 'Payment currency not found' });
    }

    if (currency.type !== 'credit') {
      return res.status(400).json({ error: 'Only credit currencies can have token_config' });
    }

    if (currency.token_config) {
      return res.status(400).json({ error: 'Token config already exists. Cannot be updated once set.' });
    }

    const paymentMethod = await PaymentMethod.findOne({
      where: {
        livemode: currency.livemode,
        type: 'arcblock',
      },
    });

    if (!paymentMethod) {
      return res.status(400).json({ error: 'ArcBlock payment method not found' });
    }

    const client = paymentMethod.getOcapClient();
    const { state: tokenFactoryState } = await client.getTokenFactoryState({
      address: value.token_factory_address,
    });

    if (!tokenFactoryState) {
      return res.status(400).json({ error: 'Token factory not found on chain' });
    }

    const tokenConfig = {
      address: tokenFactoryState.token.address,
      symbol: tokenFactoryState.token.symbol,
      name: tokenFactoryState.token.name,
      decimal: tokenFactoryState.token.decimal,
      token_factory_address: tokenFactoryState.address,
    };

    // Only update token_config, keep the original decimal to avoid breaking existing credit grants
    await currency.update({
      token_config: tokenConfig,
    });

    logger.info('Payment currency token_config updated', {
      currencyId: id,
      tokenConfig,
    });

    return res.json(currency.toJSON());
  } catch (err) {
    logger.error('update payment currency token_config failed', { error: err?.message, id: req.params.id });
    return res.status(400).json({ error: err?.message });
  }
});

router.delete('/:id/token-config', auth, async (req, res) => {
  try {
    const { id } = req.params;

    const currency = await PaymentCurrency.findByPk(id);
    if (!currency) {
      return res.status(404).json({ error: 'Payment currency not found' });
    }

    if (currency.type !== 'credit') {
      return res.status(400).json({ error: 'Only credit currencies can have token_config' });
    }

    if (!currency.token_config) {
      return res.status(400).json({ error: 'Token config does not exist' });
    }

    await currency.update({
      token_config: null,
    });

    logger.info('Payment currency token_config removed', {
      currencyId: id,
    });

    return res.json(currency.toJSON());
  } catch (err) {
    logger.error('delete payment currency token_config failed', { error: err?.message, id: req.params.id });
    return res.status(400).json({ error: err?.message });
  }
});

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

  const currency = await PaymentCurrency.findByPk(id);
  if (!currency) {
    return res.status(404).json({ error: 'Payment currency not found' });
  }
  const isLocked = await currency.isLocked();
  if (isLocked) {
    return res.status(400).json({ error: 'Can not delete locked payment currency' });
  }
  const isUsed = await currency.isUsed();
  if (isUsed) {
    return res.status(400).json({ error: 'Can not delete payment currency used by other resources' });
  }
  try {
    await currency.destroy();
    return res.status(200).end();
  } catch (err) {
    logger.error('delete payment currency error', err);
    return res.status(400).json({ error: 'Delete payment currency failed' });
  }
});

router.get('/:id/recharge-config', user, async (req, res) => {
  try {
    const { id } = req.params;

    const currency = await PaymentCurrency.scope('withRechargeConfig').findByPk(id);

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

    if (!currency.recharge_config) {
      return res.json({
        currency_id: id,
        recharge_config: null,
        message: 'No recharge config available for this currency',
      });
    }

    const paymentUrl = await getRechargePaymentUrl(currency);

    let basePrice: (Price & { product: Product }) | null = null;
    if (currency.recharge_config.base_price_id) {
      basePrice = (await Price.findByPk(currency.recharge_config.base_price_id, {
        include: [{ model: Product, as: 'product' }],
      })) as Price & { product: Product };
    }

    return res.json({
      currency_id: id,
      currency_info: pick(currency, ['id', 'name', 'symbol', 'decimal', 'type']),
      recharge_config: {
        ...currency.recharge_config,
        basePrice,
        payment_url: paymentUrl,
      },
    });
  } catch (error: any) {
    logger.error('Failed to get currency recharge config', {
      currencyId: req.params.id,
      error: error.message,
    });
    return res.status(500).json({ error: 'Internal server error' });
  }
});

const rechargeConfigSchema = Joi.object({
  base_price_id: Joi.string().required(),
  payment_link_id: Joi.string().optional(),
  checkout_url: Joi.string().optional(),
  settings: Joi.object({
    min_recharge_amount: Joi.number().min(0).optional(),
    max_recharge_amount: Joi.number().min(0).optional(),
  }).optional(),
}).unknown(true);

router.put('/:id/recharge-config', auth, async (req, res) => {
  const { id } = req.params;
  const { error, value: rechargeConfig } = rechargeConfigSchema.validate(
    pick(req.body, ['base_price_id', 'payment_link_id', 'checkout_url', 'settings'])
  );
  if (error) {
    return res.status(400).json({ error: error.message });
  }

  const currency = await PaymentCurrency.findByPk(id);

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

  if (rechargeConfig.base_price_id) {
    const basePrice = (await Price.findOne({
      where: {
        id: rechargeConfig.base_price_id,
        active: true,
      },
      include: [{ model: Product, as: 'product' }],
    })) as Price & { product: Product };

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

  await currency.update({ recharge_config: rechargeConfig });

  logger.info('Currency recharge config updated', {
    currencyId: id,
    basePriceId: rechargeConfig.base_price_id,
    tiersCount: rechargeConfig.price_tiers?.length || 0,
  });

  return res.json({
    currency_id: id,
    recharge_config: rechargeConfig,
    message: 'Recharge config updated successfully',
  });
});

// check if currencies support recurring subscriptions
router.post('/check-recurring-support', async (req, res) => {
  try {
    const { currency_ids: currencyIds } = req.body;
    if (!Array.isArray(currencyIds)) {
      return res.status(400).json({ error: 'currency_ids must be an array' });
    }
    if (currencyIds.length === 0) {
      return res.status(400).json({ error: 'currency_ids cannot be empty' });
    }
    const { notSupportCurrencies, validate } = await checkCurrencySupportRecurring(currencyIds, true);
    return res.json({
      supported: validate,
      unsupported_currencies: notSupportCurrencies.map((c) => ({
        id: c.id,
        name: c.name,
        symbol: c.symbol,
      })),
    });
  } catch (err) {
    logger.error('check recurring support error', err);
    return res.status(400).json({ error: err.message });
  }
});

export default router;
