import { BN } from '@ocap/util';

import { Op } from 'sequelize';
import createQueue from '../libs/queue';
import {
  AutoRechargeConfig,
  ChainType,
  CreditGrant,
  Customer,
  Invoice,
  Meter,
  PaymentCurrency,
  PaymentMethod,
  Price,
  PriceQuote,
  Product,
  TPriceExpanded,
} from '../store/models';
import logger from '../libs/logger';
import { createEvent } from '../libs/audit';
import { createStripeInvoiceForAutoRecharge } from '../integrations/stripe/resource';
import { ensureInvoiceAndItems } from '../libs/invoice';
import dayjs from '../libs/dayjs';
import { invoiceQueue } from './invoice';
import { getPriceUintAmountByCurrency } from '../libs/price';
import { getExchangeRateService } from '../libs/exchange-rate/service';
import { getExchangeRateSymbol, hasTokenAddress } from '../libs/exchange-rate/token-address-mapping';
import { isRateBelowMinAcceptableRate } from '../libs/slippage';
import { getQuoteService } from '../libs/quote-service';
import { events } from '../libs/event';
import { isDelegationSufficientForPayment } from '../libs/payment';

export interface AutoRechargeJobData {
  customer_id: string;
  currency_id: string;
}

export interface AutoRechargeJobResult {
  success: boolean;
  invoice_id?: string;
  error_message?: string;
  recharge_amount?: string;
  credit_amount?: string;
  payment_method_type?: string;
}

export async function processAutoRecharge(job: AutoRechargeJobData) {
  logger.info('Processing auto recharge job', { job });
  const { customer_id: customerId, currency_id: currencyId } = job;

  const customer = await Customer.findByPk(customerId);
  if (!customer) {
    logger.error('Customer not found', { customerId });
    return;
  }

  const currency = await PaymentCurrency.findByPk(currencyId);
  if (!currency) {
    logger.error('Currency not found', { currencyId });
    return;
  }

  // Check if the associated meter is inactive
  if (currency.type === 'credit') {
    // Find meter by currency_id (meter.currency_id -> PaymentCurrency) or by metadata.meter_id
    const meter = await Meter.findOne({
      where: { currency_id: currencyId },
    });
    if (meter && meter.status === 'inactive') {
      logger.info('Meter is inactive, skipping auto recharge', {
        customerId,
        currencyId,
        meterId: meter.id,
      });
      return;
    }
  }

  // 1. find auto recharge config
  const config = (await AutoRechargeConfig.findOne({
    where: {
      customer_id: customerId,
      currency_id: currencyId,
    },
    include: [
      { model: PaymentCurrency, as: 'rechargeCurrency', required: false },
      { model: Price, as: 'price', include: [{ model: Product, as: 'product' }] },
      { model: PaymentMethod, as: 'paymentMethod' },
    ],
  })) as AutoRechargeConfig & {
    paymentMethod: PaymentMethod;
    price: TPriceExpanded;
    rechargeCurrency: PaymentCurrency;
  };

  if (!config || !config.enabled) {
    logger.info('No auto recharge config found', { customerId, currencyId });
    return;
  }

  if (!config.rechargeCurrency) {
    logger.error('Recharge currency not found', { customerId, currencyId });
    return;
  }

  const availableGrants = await CreditGrant.getAvailableCreditsForCustomer(customerId, currencyId, [config.price_id]);

  const totalAvailable = availableGrants.reduce((sum, grant) => sum.add(new BN(grant.remaining_amount)), new BN(0));

  // 2. check if available balance is above threshold
  if (new BN(totalAvailable).gt(new BN(config.threshold))) {
    logger.info('Available balance is above threshold, skipping auto recharge', {
      customerId,
      currencyId,
      totalAvailable: totalAvailable.toString(),
      threshold: config.threshold,
    });
    return;
  }

  const priceAmount = await getPriceUintAmountByCurrency(config.price, config.rechargeCurrency.id);
  if (!priceAmount) {
    logger.error('Price amount is not valid', {
      customerId,
      currencyId,
    });
    return;
  }
  const totalAmount = new BN(priceAmount).mul(new BN(config.quantity ?? 0)).toString();

  // 3. check daily limit
  if (!config.canRechargeToday(totalAmount)) {
    logger.info('Daily recharge limit exceeded, skipping auto recharge', {
      customerId,
      currencyId,
    });
    // Send notification when daily limit is exceeded
    createEvent('Customer', 'customer.auto_recharge.daily_limit_exceeded', customer, {
      autoRechargeConfigId: config.id,
      currencyId,
      currentBalance: totalAvailable.toString(),
    });
    return;
  }

  // 4. Check dynamic pricing constraints
  const isDynamicPricing = config.price.pricing_type === 'dynamic';
  let rateResult: DynamicPricingCheckResult['rateResult'];

  if (isDynamicPricing) {
    const dynamicPricingResult = await checkDynamicPricingConstraints({
      customer,
      config,
    });

    if (!dynamicPricingResult.canProceed) {
      // Emit event for notification
      events.emit('auto_recharge.skipped', {
        config_id: config.id,
        customer_id: customerId,
        reason: dynamicPricingResult.reason,
        current_rate: dynamicPricingResult.currentRate,
        min_acceptable_rate: config.slippage_config?.min_acceptable_rate,
        // Additional context for notification
        payment_currency_symbol: config.rechargeCurrency.symbol,
        payment_currency_name: config.rechargeCurrency.name,
        credit_currency_name: currency.name,
      });

      logger.info('Auto recharge skipped due to dynamic pricing constraints', {
        customerId,
        currencyId,
        reason: dynamicPricingResult.reason,
        currentRate: dynamicPricingResult.currentRate,
        minAcceptableRate: config.slippage_config?.min_acceptable_rate,
      });
      return;
    }

    rateResult = dynamicPricingResult.rateResult;
  }

  // 5. execute auto recharge
  await executeAutoRecharge(customer, config, currency, rateResult);
}

interface DynamicPricingCheckResult {
  canProceed: boolean;
  reason?: 'slippage_exceeded' | 'exchange_rate_not_supported' | 'exchange_rate_fetch_failed';
  currentRate?: string;
  rateResult?: {
    rate: string;
    provider_id: string;
    provider_name: string;
    timestamp_ms: number;
    degraded: boolean;
    consensus_method?: string;
    providers?: Array<{
      provider_id: string;
      provider_name: string;
      rate: string;
      timestamp_ms: number;
      degraded: boolean;
    }>;
  };
}

/**
 * Check if dynamic pricing constraints are met before creating quotes
 * This only validates exchange rate availability and slippage threshold
 * Quotes are created later when building the invoice
 */
async function checkDynamicPricingConstraints({
  customer,
  config,
}: {
  customer: Customer;
  config: AutoRechargeConfig & {
    paymentMethod: PaymentMethod;
    price: TPriceExpanded;
    rechargeCurrency: PaymentCurrency;
  };
}): Promise<DynamicPricingCheckResult> {
  const methodType = config.paymentMethod.type as ChainType;

  // Check if exchange rate is supported
  if (!hasTokenAddress(config.rechargeCurrency.symbol, methodType)) {
    return {
      canProceed: false,
      reason: 'exchange_rate_not_supported',
    };
  }

  // Get current exchange rate
  let rateResult;
  try {
    const exchangeRateService = getExchangeRateService();
    const rateSymbol = getExchangeRateSymbol(config.rechargeCurrency.symbol, methodType);
    rateResult = await exchangeRateService.getRate(rateSymbol);
  } catch (error: any) {
    logger.warn('Failed to fetch exchange rate for auto-recharge', {
      error: error.message,
      symbol: config.rechargeCurrency.symbol,
      customerId: customer.id,
    });
    return {
      canProceed: false,
      reason: 'exchange_rate_fetch_failed',
    };
  }

  // Check slippage if config exists
  if (config.slippage_config?.min_acceptable_rate) {
    const rateBelowMin = isRateBelowMinAcceptableRate(rateResult.rate, config.slippage_config.min_acceptable_rate);

    if (rateBelowMin) {
      return {
        canProceed: false,
        reason: 'slippage_exceeded',
        currentRate: rateResult.rate,
      };
    }
  }

  return {
    canProceed: true,
    currentRate: rateResult.rate,
    rateResult,
  };
}

/**
 * Create quotes for each line item and enrich them with quote info
 * This follows the same pattern as attachQuotesForInvoice in subscription.ts
 */
async function attachQuotesForAutoRecharge(
  lineItems: any[],
  invoiceId: string,
  config: AutoRechargeConfig & {
    rechargeCurrency: PaymentCurrency;
    slippage_config?: { percent?: number; min_acceptable_rate?: string };
  },
  rateResult: DynamicPricingCheckResult['rateResult']
): Promise<{
  enrichedLineItems: any[];
  quotes: PriceQuote[];
  totalAmount: BN;
}> {
  const quoteService = getQuoteService();
  const slippagePercent = config.slippage_config?.percent;

  // Create quotes for each line item
  const quoteResults = await Promise.all(
    lineItems.map(async (item) => {
      const quoteResponse = await quoteService.createQuoteWithRate({
        price_id: item.price_id,
        invoice_id: invoiceId,
        target_currency_id: config.recharge_currency_id!,
        quantity: item.quantity,
        rateResult: rateResult!,
        slippage_percent: slippagePercent,
      });
      return { item, quoteResponse };
    })
  );

  // Build quote map for lookup
  const quoteMap = new Map<string, (typeof quoteResults)[number]>();
  quoteResults.forEach((result) => {
    quoteMap.set(result.item.price_id, result);
  });

  // Enrich line items with quote info
  // Only store quote_id in metadata - full quote info is attached dynamically when querying
  // See: invoices.ts attachQuoteMetadataToLines()
  const enrichedLineItems = lineItems.map((item) => {
    const hit = quoteMap.get(item.price_id);
    if (!hit) {
      return item;
    }
    const { quoteResponse } = hit;
    return {
      ...item,
      quote_id: quoteResponse.quote.id,
      custom_amount: quoteResponse.computed_unit_amount,
      // Only store quote_id in metadata, not the full quote object
      metadata: {
        ...(item.metadata || {}),
        quote_id: quoteResponse.quote.id,
      },
    };
  });

  // Calculate total amount from quotes
  const totalAmount = quoteResults.reduce((sum, result) => {
    return sum.add(new BN(result.quoteResponse.computed_unit_amount));
  }, new BN(0));

  const quotes = quoteResults.map((r) => r.quoteResponse.quote);

  logger.info('Created quotes for auto-recharge line items', {
    configId: config.id,
    quoteIds: quotes.map((q) => q.id),
    totalAmount: totalAmount.toString(),
    slippagePercent,
  });

  return { enrichedLineItems, quotes, totalAmount };
}

async function createInvoiceForAutoRecharge({
  customer,
  config,
  currency,
  price,
  paymentMethod,
  rechargeCurrency,
  isDynamicPricing,
  rateResult,
}: {
  customer: Customer;
  config: AutoRechargeConfig;
  currency: PaymentCurrency;
  price: TPriceExpanded;
  paymentMethod: PaymentMethod;
  rechargeCurrency: PaymentCurrency;
  isDynamicPricing: boolean;
  rateResult?: DynamicPricingCheckResult['rateResult'];
}) {
  const now = dayjs().unix();
  const expandedItems = await Price.expand([{ price_id: price.id, quantity: config.quantity }], {
    product: true,
  });

  let status = 'open';
  if (paymentMethod.type === 'stripe') {
    status = 'draft'; // stripe invoice will be finalized and paid automatically
  }

  // Check for existing invoice
  const existInvoice = await Invoice.findOne({
    where: {
      customer_id: customer.id,
      currency_id: rechargeCurrency.id,
      billing_reason: 'auto_recharge',
      status: {
        [Op.in]: ['open', 'draft', 'uncollectible'],
      },
    },
  });
  if (existInvoice) {
    logger.info('Auto recharge invoice already exists, skipping', {
      customerId: customer.id,
      configId: config.id,
    });
    return { invoice: existInvoice, quotes: [] };
  }

  // For dynamic pricing, create quotes for each line item
  let lineItems = expandedItems;
  let totalAmount: BN;
  let quotes: PriceQuote[] = [];

  if (isDynamicPricing && rateResult) {
    // Generate a temporary invoice ID for quote creation
    // We'll update the quotes with the actual invoice ID after creation
    const tempInvoiceId = `temp-auto-recharge-${config.id}-${Date.now()}`;
    const quoteResult = await attachQuotesForAutoRecharge(expandedItems, tempInvoiceId, config as any, rateResult);
    lineItems = quoteResult.enrichedLineItems;
    totalAmount = quoteResult.totalAmount;
    quotes = quoteResult.quotes;
  } else {
    // Fixed pricing: calculate from price config
    const priceAmount = await getPriceUintAmountByCurrency(price, rechargeCurrency.id);
    totalAmount = new BN(priceAmount).mul(new BN(config.quantity ?? 1));
  }

  // Build metadata
  const invoiceMetadata: Record<string, any> = {
    auto_recharge: {
      config_id: config.id,
      currency_id: rechargeCurrency.id,
    },
  };

  const firstQuote = quotes.length > 0 ? quotes[0] : undefined;
  if (firstQuote) {
    invoiceMetadata.quote_ids = quotes.map((q) => q.id);
    // Also store the first quote's info for backwards compatibility
    invoiceMetadata.quote = {
      quote_id: firstQuote.id,
      exchange_rate: firstQuote.exchange_rate,
      quoted_amount: firstQuote.quoted_amount,
      max_payable_token: firstQuote.max_payable_token,
      slippage_percent: firstQuote.slippage_percent,
    };
  }

  const { invoice } = await ensureInvoiceAndItems({
    customer,
    currency: rechargeCurrency,
    trialing: false,
    metered: false,
    lineItems,
    applyCredit: false,
    props: {
      status,
      total: totalAmount.toString(),
      livemode: rechargeCurrency.livemode,
      description: `Auto Top-up for ${currency.name}`,
      statement_descriptor: '',
      period_start: now,
      period_end: now,
      auto_advance: true,
      billing_reason: 'auto_recharge',
      currency_id: rechargeCurrency.id,
      default_payment_method_id: paymentMethod.id,
      custom_fields: [],
      footer: '',
      payment_settings: config.payment_settings,
      metadata: invoiceMetadata,
    } as unknown as Invoice,
  });

  // Link all quotes to the actual invoice
  if (quotes.length > 0) {
    await Promise.all(quotes.map((q) => q.update({ invoice_id: invoice.id })));
    logger.info('Quotes linked to auto recharge invoice', {
      quoteIds: quotes.map((q) => q.id),
      invoiceId: invoice.id,
    });
  }

  logger.info('New invoice created for auto recharge', {
    customerId: customer.id,
    configId: config.id,
    invoiceId: invoice.id,
    amount: totalAmount.toString(),
    quoteIds: quotes.map((q) => q.id),
    isDynamicPricing: quotes.length > 0,
  });
  if (status !== 'draft') {
    invoiceQueue.push({
      id: invoice.id,
      job: { invoiceId: invoice.id, retryOnError: true },
    });
    logger.info('Invoice job scheduled for auto recharge', { invoice: invoice.id, customerId: customer.id });
  }
  return { invoice, quotes };
}

async function executeAutoRecharge(
  customer: Customer,
  config: AutoRechargeConfig & {
    paymentMethod: PaymentMethod;
    price: TPriceExpanded;
    rechargeCurrency: PaymentCurrency;
  },
  currency: PaymentCurrency,
  rateResult?: DynamicPricingCheckResult['rateResult']
) {
  const paymentMethod = config.paymentMethod!;
  const price = config.price! as TPriceExpanded;
  const rechargeCurrency = config.rechargeCurrency!;
  const isDynamicPricing = price.pricing_type === 'dynamic';

  try {
    const paymentSettings = config.payment_settings;
    if (!paymentSettings) {
      throw new Error('No payment settings found for auto recharge');
    }
    const payer = (paymentSettings.payment_method_options as any)?.[paymentMethod.type]?.payer;
    if (!payer) {
      throw new Error('No payer found for auto recharge');
    }

    // Calculate totalAmount for balance check
    const priceAmount = await getPriceUintAmountByCurrency(price, rechargeCurrency.id);
    if (!priceAmount) {
      throw new Error('Price amount is not valid');
    }
    const totalAmount = new BN(priceAmount).mul(new BN(config.quantity ?? 1)).toString();

    // Check delegation balance for non-stripe payment methods
    if (paymentMethod.type !== 'stripe') {
      const balanceCheck = await isDelegationSufficientForPayment({
        paymentMethod,
        paymentCurrency: rechargeCurrency,
        userDid: payer,
        amount: totalAmount,
        storedDelegationAddress: config.payment_details?.arcblock?.delegation_address,
      });

      if (!balanceCheck.sufficient) {
        logger.error('Insufficient balance for auto recharge', {
          customerId: customer.id,
          configId: config.id,
          reason: balanceCheck.reason,
          totalAmount,
          payer,
        });
        throw new Error(`Insufficient balance: ${balanceCheck.reason}`);
      }
    }

    const result = await createInvoiceForAutoRecharge({
      customer,
      config,
      currency,
      price,
      paymentMethod,
      rechargeCurrency,
      isDynamicPricing,
      rateResult,
    });

    // Handle both return types (existing invoice or new invoice)
    const invoice = 'invoice' in result ? result.invoice : result;

    if (paymentMethod.type === 'stripe') {
      await createStripeInvoiceForAutoRecharge({
        autoRechargeConfig: config,
        customer,
        paymentMethod,
        currency: rechargeCurrency,
        invoice,
      });
    }
  } catch (error: any) {
    logger.error('Auto recharge execution failed', {
      customerId: customer.id,
      configId: config.id,
      paymentMethodType: paymentMethod.type,
      error: error.message,
    });
    throw error;
  }
}

// 创建自动充值队列
export const autoRechargeQueue = createQueue<AutoRechargeJobData>({
  name: 'auto-recharge',
  onJob: processAutoRecharge,
  options: {
    concurrency: 5,
    maxRetries: 3,
  },
});

/**
 * 检查并触发自动充值 (添加到队列)
 */
// Q2: TTL cache for AutoRechargeConfig existence check (99%+ returns null, rarely changes)
const AUTO_RECHARGE_CACHE_TTL = 5 * 60_000; // 5 minutes
const AUTO_RECHARGE_CACHE_MAX_SIZE = 500;
const autoRechargeConfigCache = new Map<string, { exists: boolean; expires: number }>();

let autoRechargeHooksRegistered = false;
function ensureAutoRechargeHooks() {
  if (autoRechargeHooksRegistered) return;
  autoRechargeHooksRegistered = true;
  const invalidateCache = (model: any) => {
    autoRechargeConfigCache.delete(`${model.customer_id}:${model.currency_id}`);
  };
  AutoRechargeConfig.addHook('afterCreate', 'invalidateAutoRechargeCache', invalidateCache);
  AutoRechargeConfig.addHook('afterUpdate', 'invalidateAutoRechargeCacheOnUpdate', invalidateCache);
  AutoRechargeConfig.addHook('afterDestroy', 'invalidateAutoRechargeCacheOnDelete', invalidateCache);
}

export async function checkAndTriggerAutoRecharge(
  customer: Customer,
  currencyId: string,
  currentBalance: string,
  options?: { currency?: any; meter?: any }
): Promise<void> {
  try {
    logger.info('----- Checking and triggering auto recharge -----', {
      customerId: customer.id,
      currencyId,
      currentBalance,
    });

    // Q2: Check cache first — most customers have no auto-recharge config
    ensureAutoRechargeHooks();
    const cacheKey = `${customer.id}:${currencyId}`;
    const cached = autoRechargeConfigCache.get(cacheKey);
    if (cached && cached.expires > Date.now() && !cached.exists) {
      logger.debug('Auto recharge config not found (cached)', {
        customerId: customer.id,
        currencyId,
      });
      return;
    }

    // T2b: Reuse caller-provided currency to avoid redundant DB query
    const currency = options?.currency ?? (await PaymentCurrency.findByPk(currencyId));

    // Reuse caller-provided meter when available to avoid redundant DB query
    let meterPromise: Promise<any>;
    if (options?.meter != null) {
      meterPromise = Promise.resolve(options.meter);
    } else if (currency?.type === 'credit') {
      meterPromise = Meter.findOne({ where: { currency_id: currencyId } });
    } else {
      meterPromise = Promise.resolve(null);
    }

    const [meter, config] = await Promise.all([
      meterPromise,
      AutoRechargeConfig.findOne({
        where: {
          customer_id: customer.id,
          currency_id: currencyId,
          enabled: true,
        },
      }),
    ]);

    // Update cache with result (evict expired entries if cache grows too large)
    if (autoRechargeConfigCache.size > AUTO_RECHARGE_CACHE_MAX_SIZE) {
      const now = Date.now();
      for (const [k, v] of autoRechargeConfigCache) {
        if (v.expires < now) autoRechargeConfigCache.delete(k);
      }
    }
    autoRechargeConfigCache.set(cacheKey, { exists: !!config, expires: Date.now() + AUTO_RECHARGE_CACHE_TTL });

    if (meter && meter.status === 'inactive') {
      logger.info('Meter is inactive, skipping auto recharge check', {
        customerId: customer.id,
        currencyId,
        meterId: meter.id,
      });
      return;
    }

    if (!config) {
      logger.debug('No auto recharge config found', {
        customerId: customer.id,
        currencyId,
      });
      return;
    }

    logger.info('Auto recharge config found', {
      customerId: customer.id,
      currencyId,
      config,
      currentBalance,
      threshold: config.threshold,
    });

    //  check if balance is above threshold
    if (new BN(currentBalance).gte(new BN(config.threshold))) {
      logger.debug('Balance above threshold, skipping auto recharge', {
        customerId: customer.id,
        currentBalance,
        threshold: config.threshold,
      });
      return;
    }

    const jobData: AutoRechargeJobData = {
      customer_id: customer.id,
      currency_id: currencyId,
    };

    await autoRechargeQueue.push({
      job: jobData,
      id: `auto-recharge-${customer.id}-${currencyId}`,
      persist: true,
    });

    logger.info('Added auto recharge job to queue', {
      customerId: customer.id,
      currencyId,
      currentBalance,
      threshold: config.threshold,
    });
  } catch (error: any) {
    logger.error('Failed to trigger auto recharge', {
      customerId: customer.id,
      currencyId,
      currentBalance,
      error: error.message,
    });
  }
}
