import { BN, fromTokenToUnit } from '@ocap/util';
import { Router } from 'express';
import Joi from 'joi';
import cloneDeep from 'lodash/cloneDeep';
import pick from 'lodash/pick';
import { Op } from 'sequelize';

import { createListParamSchema, getOrder, getWhereFromKvQuery, getWhereFromQuery, MetadataSchema } from '../libs/api';
import logger from '../libs/logger';
import { authenticate } from '../libs/security';
import { formatMetadata } from '../libs/util';
import { PaymentCurrency } from '../store/models/payment-currency';
import { Price } from '../store/models/price';
import { Product } from '../store/models/product';
import { ProductVendor } from '../store/models/product-vendor';

import type { CustomUnitAmount } from '../store/models/types';
import { checkCurrencySupportRecurring } from '../libs/product';

const router = Router();

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

const VendorConfigSchema = Joi.array()
  .items(
    Joi.object({
      vendor_id: Joi.string().required(),
      commission_rate: Joi.number().min(0).max(100).required(),
      commission_type: Joi.string().valid('percentage', 'fixed').required(),
    }).unknown(true)
  )
  .optional();

const ProductAndPriceSchema = Joi.object({
  name: Joi.string().max(64).empty('').optional(),
  type: Joi.string().valid('service', 'good', 'credit').empty('').optional(),
  description: Joi.string().max(250).empty('').optional(),
  images: Joi.any().optional(),
  metadata: MetadataSchema,
  tax_code: Joi.string().max(30).allow(null).empty('').optional(),
  statement_descriptor: Joi.string()
    .max(22)
    .pattern(/^(?=.*[A-Za-z])[^\u4e00-\u9fa5<>"’\\]*$/)
    .messages({
      'string.pattern.base':
        'statement_descriptor should be at least one letter and cannot include Chinese characters and special characters such as <, >、"、’ or \\',
    })
    .allow(null, '')
    .empty('')
    .optional(),
  unit_label: Joi.string().max(12).allow(null).empty('').optional(),
  nft_factory: Joi.string().max(40).allow(null).empty('').optional(),
  features: Joi.array()
    .items(Joi.object({ name: Joi.string().max(64).empty('').optional() }).unknown(true))
    .optional(),
  prices: Joi.array()
    .items(
      Joi.object({
        currency_options: Joi.array()
          .items(
            Joi.object({
              unit_amount: Joi.number().greater(0).required(),
            }).unknown(true)
          )
          .optional(),
        unit_amount: Joi.number().greater(0).required(),
        nickname: Joi.string().max(64).empty('').optional(),
        lookup_key: Joi.string().max(64).empty('').optional(),
      }).unknown(true)
    )
    .optional(),
  vendor_config: VendorConfigSchema,
}).unknown(true);

// 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',
  });

export async function createProductAndPrices(payload: any) {
  const raw: Partial<Product> = pick(payload, [
    'name',
    'type',
    'description',
    'images',
    'metadata',
    'statement_descriptor',
    'unit_label',
    'nft_factory',
    'features',
    'metadata',
    'tax_code',
  ]);

  if (Array.isArray(payload.vendor_config) && payload.vendor_config.length > 0) {
    // validate vendor_config data
    const { error: vendorConfigError } = VendorConfigSchema.validate(payload.vendor_config);
    if (vendorConfigError) {
      throw new Error(`vendor_config validation failed: ${vendorConfigError.message}`);
    }

    const vendorConfigs = await ProductVendor.findAll({
      where: {
        id: payload.vendor_config.map((x: any) => x.vendor_id),
      },
    });

    raw.vendor_config = payload.vendor_config.map((config: any) => {
      const vendorConfig = vendorConfigs.find((x) => x.id === config.vendor_id);
      if (!vendorConfig) {
        throw new Error(`vendor ${config.vendor_id} not found`);
      }
      return {
        vendor_id: vendorConfig.id,
        vendor_key: vendorConfig.vendor_key,
        name: vendorConfig.name,
        description: vendorConfig.description,
        commission_rate: Number(config.commission_rate),
        commission_type: config.commission_type === 'fixed' ? 'fixed_amount' : config.commission_type,
        custom_params: {},
      };
    });
  }
  raw.active = true;
  raw.type = raw.type || 'service';
  raw.livemode = !!payload.livemode;
  raw.created_via = payload.via || 'api';
  raw.metadata = formatMetadata(raw.metadata);

  // 2. 验证并准备 prices 数据
  let pricesRaw: (Price & { model: 'string' })[] = [];
  if (Array.isArray(payload.prices) && payload.prices.length) {
    if (payload.prices.some((x: any) => !x.unit_amount && !x.custom_unit_amount)) {
      throw new Error('unit_amount or custom_unit_amount is required for price');
    }

    const currencies = await PaymentCurrency.findAll({ where: { active: true } });
    pricesRaw = payload.prices.map((price: Price & { model: 'string' }) => {
      const newPrice = { ...price };
      newPrice.active = !!raw.active;
      newPrice.livemode = !!raw.livemode;
      newPrice.currency_id = price.currency_id || payload.currency_id;

      const currency = currencies.find((x) => x.id === newPrice.currency_id);
      if (!currency) {
        throw new Error(`currency ${newPrice.currency_id} used in price not found or inactive`);
      }

      if (raw.type === 'credit') {
        const creditConfig = newPrice.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}`);
        }
        newPrice.metadata.credit_config = creditConfigValue;
      }

      if (newPrice.custom_unit_amount) {
        // @ts-ignore
        ['preset', 'maximum', 'minimum'].forEach((key: keyof CustomUnitAmount) => {
          if (newPrice.custom_unit_amount?.[key]) {
            // @ts-ignore
            newPrice.custom_unit_amount[key] = fromTokenToUnit(
              newPrice.custom_unit_amount[key] as string,
              currency.decimal
            ).toString();
          }
        });
        if (Array.isArray(newPrice.custom_unit_amount.presets)) {
          newPrice.custom_unit_amount.presets = newPrice.custom_unit_amount.presets.map((x) =>
            fromTokenToUnit(x, currency.decimal).toString()
          );
        }
      } else {
        if (!newPrice.unit_amount) {
          throw new Error('price.unit_amount is required');
        }
        newPrice.unit_amount = fromTokenToUnit(newPrice.unit_amount, currency.decimal).toString();
      }

      if (Array.isArray(newPrice.currency_options)) {
        newPrice.currency_options = Price.formatCurrencies(newPrice.currency_options, currencies);
      } else {
        newPrice.currency_options = [];
      }

      if (newPrice.currency_options.some((x) => x.currency_id === newPrice.currency_id) === false) {
        newPrice.currency_options.unshift({
          currency_id: newPrice.currency_id,
          unit_amount: newPrice.unit_amount,
          tiers: null,
          custom_unit_amount: newPrice.custom_unit_amount,
        });
      }

      return newPrice;
    });

    // 3. 验证货币是否支持 recurring
    const validationResults = await Promise.all(
      pricesRaw.map((x) => {
        const isRecurring = x.type === 'recurring';
        const currencyIds = (x.currency_options || []).map((c) => c.currency_id).filter(Boolean);
        return checkCurrencySupportRecurring(currencyIds, isRecurring);
      })
    );

    const invalidCurrencies = validationResults.filter((result) => !result.validate);
    if (invalidCurrencies.length > 0) {
      throw new Error(
        `Currency ${invalidCurrencies.flatMap((result) => result.notSupportCurrencies.map((c) => c?.name)).join(', ')} does not support recurring`
      );
    }
  }

  // 4. 所有验证通过后，创建 product
  const product = await Product.create(raw as Product);

  // 5. 创建 prices
  if (pricesRaw.length > 0) {
    const prices = await Promise.all(
      pricesRaw.map((price) => {
        price.product_id = product.id;
        return Price.insert(price);
      })
    );

    // 6. 更新 default price
    product.default_price_id = prices?.[0]?.id;
    await product.save();

    return { ...product.toJSON(), prices: prices.map((x) => x.toJSON()) };
  }

  return { ...product.toJSON(), prices: [] };
}

// FIXME: @wangshijun use schema validation
// create product and price
// eslint-disable-next-line consistent-return
router.post('/', auth, async (req, res) => {
  try {
    const { error } = ProductAndPriceSchema.validate(req.body);
    if (error) {
      return res.status(400).json({ error: `Product create request invalid: ${error.message}` });
    }
    const result = await createProductAndPrices({
      ...req.body,
      active: true,
      type: req.body.type || 'service',
      livemode: !!req.livemode,
      created_via: req.user?.via,
      currency_id: req.baseCurrency.id,
      metadata: formatMetadata(req.body.metadata),
    });
    logger.info('Product and prices created', {
      productId: result.id,
      name: result.name,
      priceCount: result.prices.length,
      requestedBy: req.user?.did,
    });
    res.json(result);
  } catch (err) {
    logger.error('create product error', err);
    return res.status(400).json({ error: err.message });
  }
});

// list products and prices
const paginationSchema = createListParamSchema<{
  active?: boolean;
  status?: string;
  name?: string;
  description?: string;
  donation?: string;
  meter_id?: string;
  type?: string;
}>({
  active: Joi.boolean().empty(''),
  status: Joi.string().empty(''),
  name: Joi.string().empty(''),
  description: Joi.string().empty(''),
  donation: Joi.string().empty(''),
  meter_id: Joi.string().empty(''),
  type: Joi.string().empty(''),
});
router.get('/', auth, async (req, res) => {
  const { page, pageSize, active, livemode, name, description, ...query } = await paginationSchema.validateAsync(
    req.query,
    { stripUnknown: false, allowUnknown: true }
  );
  const where = getWhereFromKvQuery(query.q);

  if (query.status) {
    // 兼容处理，支持 status
    where.active = query.status === 'active';
  }

  if (typeof active === 'boolean') {
    where.active = active;
  }
  if (typeof livemode === 'boolean') {
    where.livemode = livemode;
  }
  if (name) {
    where.name = name;
  }
  if (description) {
    where.description = description;
  }
  if (query.donation === 'hide') {
    where.created_via = { [Op.not]: 'donation' };
  }
  Object.keys(query)
    .filter((x) => x.startsWith('metadata.'))
    .forEach((key: string) => {
      // @ts-ignore
      where[key] = query[key];
    });

  if (query.type === 'credit') {
    where.type = 'credit';
  }
  const findOptions: any = {
    where,
    order: getOrder(req.query, [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']]),
    offset: (page - 1) * pageSize,
    limit: pageSize,
    include: [{ model: Price, as: 'prices' }],
    distinct: true,
  };

  if (query.type !== 'credit' && query.meter_id) {
    findOptions.include = [
      {
        model: Price,
        as: 'prices',
        where: {
          recurring: {
            meter_id: query.meter_id,
          },
        },
        required: true,
      },
    ];
  }

  if (query.type === 'credit' && query.meter_id) {
    findOptions.include = [
      {
        model: Price,
        as: 'prices',
        where: {
          metadata: {
            meter_id: query.meter_id,
          },
        },
        required: true,
      },
    ];
  }

  const { rows: list, count } = await Product.findAndCountAll(findOptions);

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

// search products
const searchSchema = createListParamSchema<{
  query: string;
}>({
  query: Joi.string(),
});
router.get('/search', auth, async (req, res) => {
  const { page, pageSize, query, livemode, q } = 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 Product.findAndCountAll({
    where,
    order: getOrder(req.query, [['created_at', 'DESC']]),
    offset: (page - 1) * pageSize,
    limit: pageSize,
    include: [{ model: Price, as: 'prices', separate: true }],
  });

  res.json({ count, list });
});

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

// update product
router.put('/:id', auth, async (req, res) => {
  const product = await Product.findByPk(req.params.id);

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

  const updates: Partial<Product> = pick(req.body, [
    'status',
    'name',
    'description',
    'images',
    'metadata',
    'statement_descriptor',
    'default_price_id',
    'unit_label',
    'features',
    'nft_factory',
    'metadata',
    'cross_sell',
    'tax_code',
  ]);

  if (Array.isArray(req.body.vendor_config)) {
    const { error: vendorConfigError } = VendorConfigSchema.validate(req.body.vendor_config);
    if (vendorConfigError) {
      return res.status(400).json({ error: `vendor_config validation failed: ${vendorConfigError.message}` });
    }

    const vendorConfigs = await ProductVendor.findAll({
      where: {
        id: req.body.vendor_config.map((x: any) => x.vendor_id),
      },
    });

    updates.vendor_config = req.body.vendor_config.map((config: any) => {
      const vendorConfig = vendorConfigs.find((x) => x.id === config.vendor_id);
      if (!vendorConfig) {
        throw new Error(`vendor ${config.vendor_id} not found`);
      }
      return {
        vendor_id: vendorConfig.id,
        vendor_key: vendorConfig.vendor_key,
        vendor_type: vendorConfig.vendor_type,
        name: vendorConfig.name,
        description: vendorConfig.description,
        commission_rate: Number(config.commission_rate),
        commission_type: config.commission_type === 'fixed' ? 'fixed_amount' : config.commission_type,
        custom_params: {},
      };
    });
  }
  const { error } = ProductAndPriceSchema.validate(updates);
  if (error) {
    return res.status(400).json({ error: `Product update request invalid: ${error.message}` });
  }
  if (updates.metadata) {
    updates.metadata = formatMetadata(updates.metadata);
  }
  await product.update(updates);
  logger.info('Product updated', {
    productId: product.id,
    updatedFields: Object.keys(updates),
    requestedBy: req.user?.did,
  });
  return res.json(await Product.expand(req.params.id as string));
});

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

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

  await product.update({ active: !product.active });

  // FIXME: deactivate payment-links, pricing-tables
  return res.json(await Product.expand(req.params.id as string));
});

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

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

    const prices = await Price.findAll({ where: { product_id: product.id } });
    for (const price of prices) {
      if (price.locked) {
        return res.status(403).json({ error: 'product have prices that is locked' });
      }
      // eslint-disable-next-line no-await-in-loop
      if (await price.isUsed(false)) {
        return res.status(403).json({ error: 'product have prices that is used by other resources' });
      }
    }

    await product.destroy();
    await Price.destroy({ where: { product_id: product.id } });
    logger.info('Product and associated prices deleted', {
      productId: product.id,
      requestedBy: req.user?.did,
    });
    return res.json(product);
  } catch (err) {
    logger.error('delete product error', err);
    return res.status(400).json({ error: err.message });
  }
});

// This is a dangerous operation, only allow to run with a special env variable
// Only meant to be used in DID Domain
router.post('/batch-price-update', auth, async (req, res) => {
  if (process.env.PAYMENT_CHANGE_LOCKED_PRICE !== '1') {
    return res.status(403).json({ error: 'forbidden' });
  }

  const dryRun = typeof req.body.dryRun === 'undefined' || req.body.dryRun !== '0';
  const factor = Number(req.body.factor);
  if (Number.isNaN(factor)) {
    return res.status(400).json({ error: 'factor is required' });
  }
  if (factor <= 0) {
    return res.status(400).json({ error: 'factor must be positive' });
  }
  if (!req.body.description) {
    return res.status(400).json({ error: 'description is required' });
  }
  if (!req.body.before) {
    return res.status(400).json({ error: 'before is required' });
  }

  try {
    const updated: any[] = [];
    const products = await Product.findAll({
      where: { active: false, livemode: !!req.livemode, created_at: { [Op.lt]: req.body.before } },
    });
    await Promise.all(
      products.map(async (product) => {
        if (!product.metadata?.domain) {
          return;
        }

        const prices = await Price.findAll({ where: { product_id: product.id } });
        for (const price of prices) {
          if (price.currency_id === req.baseCurrency.id) {
            const unit = new BN(price.unit_amount).div(new BN(factor)).toString();
            const options = cloneDeep(price.currency_options);
            const option = options.find((x) => x.currency_id === price.currency_id);
            if (option) {
              option.unit_amount = unit;
            }

            const metadata = price.metadata || {};
            metadata.update_history = [metadata.update_history, req.body.description].filter(Boolean).join(';');

            if (dryRun) {
              updated.push({ price: price.toJSON(), metadata, unit_amount: unit, currency_options: options });
            } else {
              // eslint-disable-next-line
              await Price.update(
                { metadata, unit_amount: unit, currency_options: options },
                { where: { id: price.id } }
              );
              updated.push(product.id);
            }
          }
        }
      })
    );

    logger.info('Batch price update completed', {
      updatedCount: updated.length,
      dryRun,
      factor,
      requestedBy: req.user?.did,
    });
    return res.json(updated);
  } catch (err) {
    logger.error('batch price update error', err);
    return res.status(400).json({ error: err.message });
  }
});

export default router;
