import { Router } from 'express';
import Joi from 'joi';

import { CustomError } from '@blocklet/error';
import { Op } from 'sequelize';
import { sessionMiddleware } from '@blocklet/sdk/lib/middlewares/session';
import { BN, fromTokenToUnit, fromUnitToToken } from '@ocap/util';
import { trimDecimals } from '../libs/math-utils';
import { applySlippageToAmount } from '../libs/slippage';
import {
  AutoRechargeConfig,
  Customer,
  EVMChainType,
  Meter,
  PaymentCurrency,
  PaymentMethod,
  Price,
  Product,
  TPriceExpanded,
} from '../store/models';
import { isDelegationSufficientForPayment } from '../libs/payment';
import { validateStripePaymentAmounts } from '../libs/session';
import { getPriceUintAmountByCurrency } from '../libs/price';
import { ensureStripeSetupIntentForAutoRecharge } from '../integrations/stripe/resource';
import logger from '../libs/logger';

const router = Router();

const slippageConfigSchema = Joi.object({
  mode: Joi.string().valid('percent', 'rate').optional(),
  // In rate mode, percent is calculated from min_acceptable_rate and can exceed 100%
  // In percent mode, percent is user-specified and should be 0-100
  percent: Joi.when('mode', {
    is: 'rate',
    then: Joi.number().min(0).optional(),
    otherwise: Joi.number().min(0).max(100).optional(),
  }),
  min_acceptable_rate: Joi.string().optional(),
  base_currency: Joi.string().optional(),
  updated_at_ms: Joi.number().optional(),
}).allow(null);

const createConfigSchema = Joi.object({
  customer_id: Joi.string().required(),
  enabled: Joi.boolean().default(false),
  threshold: Joi.number().default(0),
  currency_id: Joi.string().required(),
  recharge_currency_id: Joi.string().optional(),
  payment_method_id: Joi.string().optional(),
  price_id: Joi.string().optional(),
  quantity: Joi.number().integer().min(1).default(1),
  change_payment_method: Joi.boolean().default(false),
  daily_limits: Joi.object({
    max_attempts: Joi.number().integer().min(0).default(0).optional(),
    max_amount: Joi.number().min(0).default(0).optional(),
  }).optional(),
  slippage_config: slippageConfigSchema.optional(),
});

const getConfigSchema = Joi.object({
  currency_id: Joi.string().optional(),
});

// Helper functions for validation and retrieval
function getMinimumThreshold(currency: PaymentCurrency): string {
  const minimumThresholdBN = fromTokenToUnit('0.01', currency.decimal);
  return new BN(currency.minimum_payment_amount).lt(minimumThresholdBN)
    ? minimumThresholdBN.toString()
    : currency.minimum_payment_amount;
}

async function ensureCustomerExists(customerId: string): Promise<Customer> {
  const customer = await Customer.findByPkOrDid(customerId);
  if (!customer) {
    throw new CustomError(404, 'Customer not found');
  }
  return customer;
}

async function ensureCurrencyWithRechargeConfig(currencyId: string): Promise<PaymentCurrency> {
  const currency = await PaymentCurrency.scope('withRechargeConfig').findByPk(currencyId);
  if (!currency) {
    throw new CustomError(404, 'Payment currency not found');
  }
  if (!currency.recharge_config?.base_price_id) {
    throw new CustomError(404, 'No top-up package found');
  }
  return currency;
}

async function ensurePaymentMethodExists(paymentMethodId: string): Promise<PaymentMethod> {
  const paymentMethod = await PaymentMethod.findByPk(paymentMethodId);
  if (!paymentMethod) {
    throw new CustomError(400, `Payment method not found: ${paymentMethodId}`);
  }
  return paymentMethod;
}

// Check if slippage_config has changed in a way that requires reauthorization
function hasSlippageConfigChanged(
  oldConfig: AutoRechargeConfig['slippage_config'],
  newConfig: AutoRechargeConfig['slippage_config']
): boolean {
  // If both are empty, no change
  if (!oldConfig && !newConfig) return false;
  // If one is empty and the other is not, there's a change
  if (!oldConfig || !newConfig) return true;
  // Compare key fields that affect authorization amount
  return (
    oldConfig.mode !== newConfig.mode ||
    oldConfig.percent !== newConfig.percent ||
    oldConfig.min_acceptable_rate !== newConfig.min_acceptable_rate
  );
}

// Apply balance check results to configuration
async function applyBalanceCheckResults(
  config: AutoRechargeConfig,
  balanceResult: any,
  paymentMethod: PaymentMethod,
  enableIfSufficient = false
): Promise<void> {
  const updates: any = {};

  if (balanceResult.stripeContext) {
    updates.payment_details = {
      stripe: {
        setup_intent_id: balanceResult.stripeContext.id,
        customer_id: balanceResult.stripeContext.stripe_customer_id,
      },
    };
  }

  const existingPayer =
    config.payment_settings?.payment_method_options?.[
      paymentMethod.type as keyof typeof config.payment_settings.payment_method_options
    ]?.payer;

  if (balanceResult.sufficient && balanceResult.payer && balanceResult.payer !== existingPayer) {
    updates.payment_settings = {
      payment_method_options: {
        ...(config.payment_settings?.payment_method_options || {}),
        [paymentMethod.type]: {
          ...(config.payment_settings?.payment_method_options?.[
            paymentMethod.type as keyof typeof config.payment_settings.payment_method_options
          ] || {}),
          payer: balanceResult.payer,
        },
      },
      payment_method_types: [paymentMethod.type],
    };

    if (enableIfSufficient) {
      updates.enabled = true;
    }
  }

  if (Object.keys(updates).length > 0) {
    await config.update(updates);
  }
}

// Get auto-recharge configuration for a customer
router.get('/customer/:customerId', sessionMiddleware({ accessKey: true }), async (req, res) => {
  const { error, value } = getConfigSchema.validate(req.query, {
    stripUnknown: true,
  });
  if (error) {
    throw new CustomError(400, error.message || 'Validation error');
  }

  if (!req.user) {
    throw new CustomError(403, 'Please login to continue');
  }

  let { customerId } = req.params;
  if (!customerId) {
    customerId = req.user?.did;
  }

  const { currency_id: currencyId } = value;
  const customer = await ensureCustomerExists(customerId);

  if (customer.did !== req.user?.did && !['admin', 'owner'].includes(req.user?.role)) {
    throw new CustomError(403, 'You are not allowed to access this resource');
  }

  const where: any = {
    customer_id: customer.id, // Use customer.id instead of customerId from params
  };

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

  const currency = await ensureCurrencyWithRechargeConfig(currencyId);

  const config = (await AutoRechargeConfig.findOne({
    where,
    include: [
      {
        model: PaymentCurrency,
        as: 'rechargeCurrency',
        required: false,
        attributes: ['id', 'name', 'symbol', 'type', 'decimal', 'logo'],
      },
      {
        model: PaymentMethod,
        as: 'paymentMethod',
        attributes: ['id', 'type', 'name', 'active'],
      },
    ],
  })) as AutoRechargeConfig & { rechargeCurrency: PaymentCurrency; paymentMethod: PaymentMethod };
  const price = await Price.findByPk(currency.recharge_config?.base_price_id, {
    include: [{ model: Product, as: 'product' }],
  });
  if (!price) {
    throw new CustomError(404, 'Base price not found');
  }
  if (!config) {
    const minimumThreshold = getMinimumThreshold(currency);
    const newConfig = await AutoRechargeConfig.create({
      customer_id: customer.id, // Use customer.id instead of customerId from params
      currency_id: currencyId,
      livemode: currency.livemode,
      enabled: false,
      threshold: minimumThreshold,
      price_id: price.id,
      quantity: 1,
      recharge_currency_id: price.currency_id,
    });
    return res.json({
      ...newConfig.toJSON(),
      currency,
      price: {
        ...price.toJSON(),
        pricing_type: price.pricing_type || 'fixed',
      },
      threshold: fromUnitToToken(minimumThreshold, currency.decimal),
      customer,
    });
  }

  if (config.price_id !== price.id) {
    await config.update({
      price_id: price.id,
    });
  }
  const minimumThreshold = getMinimumThreshold(currency);
  const configThreshold = new BN(config.threshold || '0').lt(new BN(minimumThreshold))
    ? minimumThreshold
    : config.threshold;
  return res.json({
    ...config.toJSON(),
    daily_limits: {
      max_attempts: config.daily_limits?.max_attempts || 0,
      max_amount: fromUnitToToken(config.daily_limits?.max_amount || '0', config.rechargeCurrency?.decimal || 2),
    },
    currency,
    price: {
      ...price.toJSON(),
      pricing_type: price.pricing_type || 'fixed',
    },
    threshold: fromUnitToToken(configThreshold, currency.decimal),
    customer,
  });
});

// Retrieve specific auto-recharge configuration by ID
router.get('/retrieve/:id', sessionMiddleware({ accessKey: true }), async (req, res) => {
  const config = await AutoRechargeConfig.findByPk(req.params.id);
  if (!config) {
    throw new CustomError(404, 'Auto recharge config not found');
  }
  const paymentMethod = await PaymentMethod.findByPk(config.payment_method_id);
  if (!paymentMethod) {
    throw new CustomError(404, 'Payment method not found');
  }
  return res.json({
    ...config.toJSON(),
    paymentMethod,
  });
});

async function checkSufficientBalance({
  price,
  quantity = 1,
  rechargeCurrency,
  userDid,
  customer,
  autoRechargeConfig,
  forceReauthorize = false,
}: {
  price: Price;
  quantity: number;
  rechargeCurrency: PaymentCurrency;
  userDid: string;
  customer: Customer;
  autoRechargeConfig: AutoRechargeConfig;
  forceReauthorize?: boolean;
}) {
  const priceAmount = await getPriceUintAmountByCurrency(price, rechargeCurrency.id);
  let amount = new BN(priceAmount).mul(new BN(quantity));

  // Apply slippage buffer for dynamic pricing authorization
  // This ensures the delegation covers the maximum possible payment amount
  const slippagePercent = autoRechargeConfig.slippage_config?.percent;
  if (slippagePercent && slippagePercent > 0) {
    amount = applySlippageToAmount(amount, slippagePercent);
    logger.info('Applied slippage to authorization amount', {
      baseAmount: new BN(priceAmount).mul(new BN(quantity)).toString(),
      slippagePercent,
      authorizedAmount: amount.toString(),
    });
  }

  const paymentMethod = await PaymentMethod.findByPk(rechargeCurrency.payment_method_id);

  if (!paymentMethod) {
    throw new CustomError(400, `Payment method not found: ${rechargeCurrency.payment_method_id}`);
  }

  const payer =
    autoRechargeConfig.payment_settings?.payment_method_options?.[
      paymentMethod?.type as 'arcblock' | EVMChainType | 'stripe'
    ]?.payer || userDid;

  if (paymentMethod.type === 'stripe') {
    if (forceReauthorize || !autoRechargeConfig.payment_settings?.payment_method_options?.stripe?.payer) {
      const setupIntent = await ensureStripeSetupIntentForAutoRecharge(
        customer,
        paymentMethod,
        autoRechargeConfig,
        forceReauthorize
      );

      return {
        sufficient: false,
        stripeContext: {
          type: 'setup_intent',
          id: setupIntent.id,
          client_secret: setupIntent.client_secret,
          intent_type: 'setup_intent',
          status: setupIntent.status,
          stripe_customer_id: setupIntent.customer as string,
        },
      };
    }
    return {
      sufficient: true,
      payer,
    };
  }
  if (forceReauthorize) {
    return {
      sufficient: false,
      delegation: {
        sufficient: false,
        reason: 'NEED_REAUTHORIZE',
      },
    };
  }
  const delegation = await isDelegationSufficientForPayment({
    paymentMethod,
    paymentCurrency: rechargeCurrency,
    userDid: payer,
    amount: amount.toString(),
    storedDelegationAddress: autoRechargeConfig.payment_details?.arcblock?.delegation_address,
  });

  return {
    sufficient: delegation.sufficient,
    delegation,
    payer,
  };
}

// Submit auto-recharge configuration
router.post('/submit', async (req, res) => {
  const { error, value } = createConfigSchema.validate(req.body, {
    stripUnknown: true,
  });
  if (error) {
    throw new CustomError(400, error.message || 'Validation error');
  }

  const { customer_id: customerId, ...configData } = value;

  // Validate all required entities
  const customer = await ensureCustomerExists(customerId);
  const currency = await PaymentCurrency.scope('withRechargeConfig').findByPk(value.currency_id);
  if (!currency) {
    throw new CustomError(400, `Currency not found: ${value.currency_id}`);
  }

  // Check if the associated meter is active when enabling auto-recharge
  if (configData.enabled && currency.type === 'credit') {
    const meter = await Meter.findOne({
      where: { currency_id: value.currency_id },
    });
    if (meter && meter.status === 'inactive') {
      throw new CustomError(400, 'Cannot enable auto top-up: the associated meter is inactive');
    }
  }

  if (currency.recharge_config?.base_price_id && currency.recharge_config?.base_price_id !== value.price_id) {
    throw new CustomError(400, 'Price is not the base price');
  }

  const rechargeCurrency = await PaymentCurrency.findByPk(value.recharge_currency_id);
  if (!rechargeCurrency) {
    throw new CustomError(400, `Recharge currency not found: ${value.recharge_currency_id}`);
  }

  const paymentMethod = await ensurePaymentMethodExists(rechargeCurrency.payment_method_id);

  const price = (await Price.findOne({
    where: {
      [Op.or]: [{ id: value.price_id }, { lookup_key: value.price_id }],
      active: true,
    },
    include: [{ model: Product, as: 'product' }],
  })) as TPriceExpanded | null;

  if (!price) {
    throw new CustomError(400, `Active price not found: ${value.price_id}`);
  }
  let { threshold, daily_limits: dailyLimits } = configData;
  if (threshold) {
    threshold = fromTokenToUnit(trimDecimals(threshold, currency.decimal), currency.decimal).toString();
    const minimumThreshold = getMinimumThreshold(currency);
    if (new BN(threshold).lt(new BN(minimumThreshold))) {
      throw new CustomError(
        400,
        `Threshold must be greater than or equal to ${fromUnitToToken(minimumThreshold, currency.decimal)}`
      );
    }
  }
  if (dailyLimits) {
    dailyLimits = {
      max_attempts: dailyLimits.max_attempts || 0,
      max_amount: fromTokenToUnit(
        trimDecimals(dailyLimits.max_amount || '0', rechargeCurrency.decimal),
        rechargeCurrency.decimal
      ).toString(),
    };
  }
  if (paymentMethod.type === 'stripe' && configData.enabled) {
    const priceAmount = getPriceUintAmountByCurrency(price, rechargeCurrency.id);
    if (!priceAmount) {
      throw new CustomError(400, 'Price amount is not valid');
    }
    const result = validateStripePaymentAmounts(
      new BN(priceAmount).mul(new BN(Number(configData.quantity || '0'))).toString(),
      rechargeCurrency
    );
    if (!result) {
      throw new CustomError(400, 'Auto Top-up amount must be greater or equal to 0.5 USD');
    }
  }

  const existingConfig = await AutoRechargeConfig.findOne({
    where: {
      customer_id: customer.id,
      currency_id: value.currency_id,
    },
  });

  if (existingConfig) {
    if (!configData.enabled) {
      await existingConfig.update({
        enabled: false,
      });
      return res.json({
        ...existingConfig.toJSON(),
        paymentMethod,
        customer,
      });
    }
    // if exist, update it
    const updateData: any = {
      ...configData,
      currency_id: value.currency_id,
      recharge_currency_id: value.recharge_currency_id,
      price_id: price.id,
      payment_method_id: rechargeCurrency.payment_method_id,
      threshold: threshold ?? existingConfig.threshold,
      daily_limits: dailyLimits,
    };
    // Only update slippage_config if explicitly provided (including null to clear it)
    if ('slippage_config' in value) {
      updateData.slippage_config = value.slippage_config;
    }

    // Check if slippage_config changed BEFORE updating (to compare old vs new)
    const slippageConfigChanged =
      'slippage_config' in value && hasSlippageConfigChanged(existingConfig.slippage_config, value.slippage_config);

    await existingConfig.update(updateData);

    // If slippage_config changed, need to reauthorize because authorization amount may change
    const balanceResult = await checkSufficientBalance({
      price: price as unknown as Price,
      quantity: value.quantity,
      rechargeCurrency,
      userDid: customer.did,
      customer,
      autoRechargeConfig: existingConfig,
      forceReauthorize: value.change_payment_method || slippageConfigChanged,
    });
    // Update payment details and settings based on balance check result
    await applyBalanceCheckResults(existingConfig, balanceResult, paymentMethod);
    return res.json({
      ...existingConfig.toJSON(),
      balanceResult,
      paymentMethod,
      customer,
    });
  }

  const config = await AutoRechargeConfig.create({
    customer_id: customer.id, // Use customer.id instead of customerId from params
    currency_id: value.currency_id,
    recharge_currency_id: value.recharge_currency_id,
    price_id: price.id,
    livemode: currency.livemode,
    enabled: configData.enabled ?? false,
    threshold: threshold || '0',
    quantity: Number(configData.quantity || '1'),
    payment_method_id: rechargeCurrency.payment_method_id,
    daily_limits: dailyLimits,
    slippage_config: value.slippage_config,
  });

  logger.info('Auto recharge config created', {
    configId: config.id,
    customerId: customer.id,
    currencyId: value.currency_id,
  });

  const balanceResult = await checkSufficientBalance({
    price: price as unknown as Price,
    quantity: value.quantity,
    rechargeCurrency,
    userDid: customer.did,
    customer,
    autoRechargeConfig: config as AutoRechargeConfig,
    forceReauthorize: value.change_payment_method,
  });

  // Update payment details and settings based on balance check result
  await applyBalanceCheckResults(config, balanceResult, paymentMethod, true);
  return res.json({
    ...config.toJSON(),
    balanceResult,
    paymentMethod,
    customer,
  });
});

export default router;
