import { BN, fromTokenToUnit, fromUnitToToken } from '@ocap/util';
import crypto from 'crypto';

import { ChainType, PaymentCurrency, PaymentMethod, Price, PriceQuote } from '../store/models';
import type { CreateQuoteInput } from '../store/models/price-quote';
import { QuoteMetadata } from '../store/models/types';
import { getExchangeRateService, validateForSubmit } from './exchange-rate';
import logger from './logger';
import { getExchangeRateSymbol } from './exchange-rate/token-address-mapping';
import { trimDecimals, limitTokenPrecision } from './math-utils';
import { NonRetryableError } from './error';

export interface CreateQuoteParams {
  price_id: string;
  session_id?: string;
  invoice_id?: string;
  target_currency_id: string;
  quantity: number;
  idempotency_key_salt?: string;
  slippage_percent?: number;
  /** Minimum acceptable exchange rate (USD per token). Takes priority over slippage_percent for max_payable_token calculation. */
  min_acceptable_rate?: string;
}

export interface QuoteResponse {
  quote: PriceQuote;
  computed_unit_amount: string;
  expires_at: number;
}

export interface RateResult {
  rate: string;
  provider_id: string;
  provider_name: string;
  timestamp_ms: number;
  degraded: boolean;
  degraded_reason?: string | null;
  providers?: Array<{
    provider_id: string;
    provider_name: string;
    rate: string;
    timestamp_ms: number;
    degraded: boolean;
    degraded_reason?: string;
  }>;
  consensus_method?: string;
}

const DEFAULT_SLIPPAGE_PERCENT = 0.5;

const normalizeSlippagePercent = (value?: number | string | null): number => {
  if (value === undefined || value === null) {
    return DEFAULT_SLIPPAGE_PERCENT;
  }
  const normalized = typeof value === 'string' ? Number(value) : value;
  if (!Number.isFinite(normalized) || normalized < 0) {
    return DEFAULT_SLIPPAGE_PERCENT;
  }
  return normalized;
};

const resolveConsensusMethod = (rateResult: RateResult): string => {
  if (rateResult.consensus_method) {
    return rateResult.consensus_method;
  }
  if (rateResult.providers && rateResult.providers.length > 1) {
    return 'median';
  }
  return 'single';
};

function isUniqueConstraintError(error: any): boolean {
  if (!error) {
    return false;
  }
  if (error.name === 'SequelizeUniqueConstraintError') {
    return true;
  }
  const message = String(error.message || '').toLowerCase();
  return message.includes('unique constraint') || message.includes('unique violation');
}

/**
 * Generate idempotency key for quote
 */
function generateIdempotencyKey(params: {
  contextId: string; // session_id or invoice_id
  priceId: string;
  currencyId: string;
  quantity: number;
  baseAmount: string;
  salt?: string;
}): string {
  const { contextId, priceId, currencyId, quantity, baseAmount, salt } = params;
  const saltPart = salt ? `:${salt}` : '';
  return crypto
    .createHash('sha256')
    .update(`${contextId}:${priceId}:${currencyId}:${quantity}:${baseAmount}${saltPart}`)
    .digest('hex')
    .substring(0, 64);
}

/**
 * Quote Service
 *
 * Handles creation and management of price quotes for dynamic pricing
 */
export class QuoteService {
  /**
   * Get or create a quote for dynamic pricing
   */
  async getOrCreateQuote(params: CreateQuoteParams): Promise<QuoteResponse> {
    const {
      price_id: priceId,
      session_id: sessionId,
      invoice_id: invoiceId,
      target_currency_id: targetCurrencyId,
      quantity,
      idempotency_key_salt: idempotencyKeySalt,
    } = params;

    // Validate params
    if (!priceId || !targetCurrencyId || !quantity) {
      throw new NonRetryableError('INVALID_PARAMS', 'price_id, target_currency_id, and quantity are required');
    }

    if (!sessionId && !invoiceId) {
      throw new NonRetryableError('INVALID_PARAMS', 'Either session_id or invoice_id must be provided');
    }

    // Load price
    const price = await Price.findByPk(priceId);
    if (!price) {
      throw new NonRetryableError('PRICE_NOT_FOUND', `Price ${priceId} not found`);
    }

    // Check if price is dynamic
    if (price.pricing_type !== 'dynamic') {
      throw new NonRetryableError('INVALID_PRICE_TYPE', `Price ${priceId} is not a dynamic price`);
    }

    if (!price.base_currency || !price.base_amount) {
      throw new NonRetryableError('INVALID_PRICE_CONFIG', `Price ${priceId} missing base_currency or base_amount`);
    }

    // Load target currency with payment method
    const currency = (await PaymentCurrency.findByPk(targetCurrencyId, {
      include: [{ model: PaymentMethod, as: 'payment_method' }],
    })) as PaymentCurrency & { payment_method: PaymentMethod };

    if (!currency) {
      throw new NonRetryableError('CURRENCY_NOT_FOUND', `Currency ${targetCurrencyId} not found`);
    }
    if (currency.payment_method?.type === 'stripe') {
      throw new NonRetryableError('CURRENCY_NOT_SUPPORTED', `Currency ${targetCurrencyId} is not supported`);
    }

    // Calculate total base amount using BN for precision
    // USD amounts are stored with 8 decimal places (e.g., "10.50" = $10.50)
    const USD_DECIMALS = 8;
    const baseAmountBN = fromTokenToUnit(trimDecimals(price.base_amount, USD_DECIMALS), USD_DECIMALS);
    const quantityBN = new BN(quantity);
    const totalBaseAmountBN = baseAmountBN.mul(quantityBN);
    const totalBaseAmountStr = fromUnitToToken(totalBaseAmountBN.toString(), USD_DECIMALS);

    // Generate idempotency key
    const contextId = sessionId || invoiceId || '';
    const idempotencyKey = generateIdempotencyKey({
      contextId,
      priceId,
      currencyId: targetCurrencyId,
      quantity,
      baseAmount: totalBaseAmountStr,
      salt: idempotencyKeySalt,
    });

    // Try to find existing quote by idempotency key
    const existingQuote = await PriceQuote.findByIdempotencyKey(idempotencyKey);
    if (existingQuote && existingQuote.canRetry()) {
      logger.debug('Reusing existing quote', {
        quoteId: existingQuote.id,
        idempotencyKey,
        status: existingQuote.status,
      });

      return {
        quote: existingQuote,
        computed_unit_amount: existingQuote.quoted_amount,
        expires_at: existingQuote.expires_at,
      };
    }

    const slippagePercent = normalizeSlippagePercent(params.slippage_percent);
    const minAcceptableRate = params.min_acceptable_rate;

    // Get exchange rate
    // For ArcBlock payment method, always use ABT for exchange rate
    const exchangeRateService = getExchangeRateService();
    const rateSymbol = getExchangeRateSymbol(currency.symbol, currency.payment_method?.type as ChainType);
    const rateResult = await exchangeRateService.getRate(rateSymbol);

    // Calculate quoted amount using pure BN arithmetic (no JS number conversion)
    // Formula: quotedAmount = ceil(totalBaseAmountBN * 10^decimals / rateBN)
    //
    // totalBaseAmountBN is already in smallest USD unit (10^8 scale)
    // rateBN is USD per token (also 10^8 scale)
    // Result: token smallest unit (10^decimals scale)

    const rateBN = fromTokenToUnit(trimDecimals(rateResult.rate, USD_DECIMALS), USD_DECIMALS);

    // Calculate: quotedAmount = ceil((totalBaseAmountBN * 10^decimals) / rateBN)
    const numerator = totalBaseAmountBN.mul(new BN(10).pow(new BN(currency.decimal)));
    const denominator = rateBN;

    // Ceiling division: ceil(a/b) = (a + b - 1) / b for integers
    const quotedAmountRaw = numerator.add(denominator).sub(new BN(1)).div(denominator);

    // Limit precision to 10 decimal places for dynamic pricing
    // This avoids meaningless precision in token amounts
    const quotedAmount = limitTokenPrecision(quotedAmountRaw, currency.decimal, 10);

    // Calculate max_payable_token
    // Priority: min_acceptable_rate > slippage_percent
    // Formula with min_acceptable_rate: max_payable_token = ceil(base_amount / min_acceptable_rate)
    // Formula with slippage_percent: max_payable_token = ceil(quoted_amount * (1 + slippage_percent/100))
    let maxPayableToken: string | undefined;
    if (minAcceptableRate && Number(minAcceptableRate) > 0) {
      // Use min_acceptable_rate: max_payable_token = base_amount / min_acceptable_rate
      const minRateBN = fromTokenToUnit(trimDecimals(minAcceptableRate, USD_DECIMALS), USD_DECIMALS);
      const maxPayableRaw = totalBaseAmountBN
        .mul(new BN(10).pow(new BN(currency.decimal)))
        .add(minRateBN)
        .sub(new BN(1))
        .div(minRateBN);
      maxPayableToken = limitTokenPrecision(maxPayableRaw, currency.decimal, 10).toString();
    } else {
      // Use slippage_percent: max_payable_token = quoted_amount * (1 + slippage_percent/100)
      const SLIPPAGE_BPS_BASE = new BN(10000);
      const slippageBps = Math.round(slippagePercent * 100);
      const multiplier = SLIPPAGE_BPS_BASE.add(new BN(slippageBps));
      maxPayableToken = quotedAmount
        .mul(multiplier)
        .add(SLIPPAGE_BPS_BASE.sub(new BN(1)))
        .div(SLIPPAGE_BPS_BASE)
        .toString();
    }

    // Calculate expiration time
    const lockDuration = price.dynamic_pricing_config?.lock_duration || 180; // Default 180 seconds (3 minutes)
    const expiresAt = Math.floor(Date.now() / 1000) + lockDuration;

    // Create quote
    const quotePayload = {
      price_id: priceId,
      session_id: sessionId || undefined,
      invoice_id: invoiceId || undefined,
      idempotency_key: idempotencyKey,
      base_currency: price.base_currency,
      base_amount: totalBaseAmountStr,
      target_currency_id: targetCurrencyId,
      rate_currency_symbol: currency.symbol,
      exchange_rate: rateResult.rate,
      quoted_amount: quotedAmount.toString(),
      slippage_percent: slippagePercent,
      max_payable_token: maxPayableToken,
      rate_provider_id: rateResult.provider_id,
      rate_provider_name: rateResult.provider_name,
      rate_timestamp_ms: rateResult.timestamp_ms,
      expires_at: expiresAt,
      status: 'active',
      metadata: {
        calculation: {
          total_base_amount_scaled: totalBaseAmountBN.toString(),
          rate_scaled: rateBN.toString(),
          quoted_amount_unit: quotedAmount.toString(),
        },
        rounding: {
          mode: 'ceil',
          token_decimals: currency.decimal,
        },
        risk: {
          anomaly_detected: false,
          degraded: rateResult.degraded,
          degraded_reason: rateResult.degraded_reason || null,
        },
        rate: {
          consensus_method: resolveConsensusMethod(rateResult),
          providers: rateResult.providers || [],
        },
        slippage: {
          percent: slippagePercent,
          min_acceptable_rate: minAcceptableRate || undefined,
          max_payable_token: maxPayableToken,
        },
        context: {
          quantity,
          base_amount_per_unit: price.base_amount,
        },
      },
    };

    // Create quote with retry logic for idempotency key collision
    const MAX_RETRIES = 3;
    let quote: PriceQuote | null = null;
    let lastError: any = null;

    // Retry loop: sequential await is required for idempotency key collision handling
    // eslint-disable-next-line no-await-in-loop
    for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
      try {
        // eslint-disable-next-line no-await-in-loop
        quote = await PriceQuote.create(quotePayload);
        break; // Success, exit loop
      } catch (error: any) {
        lastError = error;

        if (isUniqueConstraintError(error)) {
          // Try to reuse existing quote with same idempotency key
          // eslint-disable-next-line no-await-in-loop
          const existingByKey = await PriceQuote.findOne({ where: { idempotency_key: quotePayload.idempotency_key } });
          if (existingByKey && existingByKey.canRetry()) {
            logger.warn('Idempotency key collision, reusing existing quote', {
              quoteId: existingByKey.id,
              idempotencyKey: quotePayload.idempotency_key,
              attempt,
              status: existingByKey.status,
            });
            return {
              quote: existingByKey,
              computed_unit_amount: existingByKey.quoted_amount,
              expires_at: existingByKey.expires_at,
            };
          }

          // If last attempt or existing quote not found/not retryable, throw error
          if (attempt >= MAX_RETRIES - 1) {
            logger.error('Max retries exceeded for quote creation in getOrCreateQuote', {
              priceId,
              currencyId: targetCurrencyId,
              attempts: attempt + 1,
              idempotencyKey: quotePayload.idempotency_key,
            });
            break; // Will throw error after loop
          }

          // Generate new idempotency key with retry salt
          const retrySalt = `${idempotencyKeySalt || 'fallback'}:retry:${attempt}:${Date.now()}:${crypto.randomBytes(4).toString('hex')}`;
          quotePayload.idempotency_key = generateIdempotencyKey({
            contextId,
            priceId,
            currencyId: targetCurrencyId,
            quantity,
            baseAmount: totalBaseAmountStr,
            salt: retrySalt,
          });

          logger.info('Retrying quote creation with new idempotency key', {
            priceId,
            attempt: attempt + 1,
            newIdempotencyKey: quotePayload.idempotency_key,
          });
        } else {
          // Non-idempotency error, throw immediately
          throw error;
        }
      }
    }

    // If all retries failed, throw the last error
    if (!quote) {
      throw lastError || new Error('Failed to create quote after retries');
    }

    logger.info('Created new quote', {
      quoteId: quote.id,
      priceId,
      currencyId: targetCurrencyId,
      baseAmount: totalBaseAmountStr,
      rate: rateResult.rate,
      quotedAmount: quotedAmount.toString(),
      expiresAt,
    });

    return {
      quote,
      computed_unit_amount: quotedAmount.toString(),
      expires_at: expiresAt,
    };
  }

  /**
   * Create a quote using pre-fetched exchange rate
   * This is used for batch quote creation to avoid multiple rate queries
   */
  async createQuoteWithRate(params: CreateQuoteParams & { rateResult: RateResult }): Promise<QuoteResponse> {
    const {
      price_id: priceId,
      session_id: sessionId,
      invoice_id: invoiceId,
      target_currency_id: targetCurrencyId,
      quantity,
      rateResult,
      idempotency_key_salt: idempotencyKeySalt,
      slippage_percent: slippagePercentInput,
      min_acceptable_rate: minAcceptableRate,
    } = params;

    // Validate params
    if (!priceId || !targetCurrencyId || !quantity) {
      throw new NonRetryableError('INVALID_PARAMS', 'price_id, target_currency_id, and quantity are required');
    }

    if (!sessionId && !invoiceId) {
      throw new NonRetryableError('INVALID_PARAMS', 'Either session_id or invoice_id must be provided');
    }

    // Load price
    const price = await Price.findByPk(priceId);
    if (!price) {
      throw new NonRetryableError('PRICE_NOT_FOUND', `Price ${priceId} not found`);
    }

    // Check if price is dynamic
    if (price.pricing_type !== 'dynamic') {
      throw new NonRetryableError('INVALID_PRICE_TYPE', `Price ${priceId} is not a dynamic price`);
    }

    if (!price.base_currency || !price.base_amount) {
      throw new NonRetryableError('INVALID_PRICE_CONFIG', `Price ${priceId} missing base_currency or base_amount`);
    }

    // Load target currency
    const currency = await PaymentCurrency.findByPk(targetCurrencyId);
    if (!currency) {
      throw new NonRetryableError('CURRENCY_NOT_FOUND', `Currency ${targetCurrencyId} not found`);
    }

    // Calculate total base amount using BN for precision
    const USD_DECIMALS = 8;
    const baseAmountBN = fromTokenToUnit(trimDecimals(price.base_amount, USD_DECIMALS), USD_DECIMALS);
    const quantityBN = new BN(quantity);
    const totalBaseAmountBN = baseAmountBN.mul(quantityBN);
    const totalBaseAmountStr = fromUnitToToken(totalBaseAmountBN.toString(), USD_DECIMALS);

    // Generate idempotency key
    const contextId = sessionId || invoiceId || '';
    const idempotencyKey = generateIdempotencyKey({
      contextId,
      priceId,
      currencyId: targetCurrencyId,
      quantity,
      baseAmount: totalBaseAmountStr,
      salt: idempotencyKeySalt,
    });

    // Try to find existing quote by idempotency key
    const existingQuote = await PriceQuote.findByIdempotencyKey(idempotencyKey);
    if (existingQuote && existingQuote.canRetry()) {
      logger.debug('Reusing existing quote', {
        quoteId: existingQuote.id,
        idempotencyKey,
        status: existingQuote.status,
      });

      return {
        quote: existingQuote,
        computed_unit_amount: existingQuote.quoted_amount,
        expires_at: existingQuote.expires_at,
      };
    }

    // Calculate quoted amount using pure BN arithmetic
    const rateBN = fromTokenToUnit(trimDecimals(rateResult.rate, USD_DECIMALS), USD_DECIMALS);

    // Calculate: quotedAmount = ceil((totalBaseAmountBN * 10^decimals) / rateBN)
    const numerator = totalBaseAmountBN.mul(new BN(10).pow(new BN(currency.decimal)));
    const denominator = rateBN;

    // Ceiling division: ceil(a/b) = (a + b - 1) / b
    const quotedAmountRaw = numerator.add(denominator).sub(new BN(1)).div(denominator);

    // Limit precision to 10 decimal places for dynamic pricing
    // This avoids meaningless precision in token amounts
    const quotedAmount = limitTokenPrecision(quotedAmountRaw, currency.decimal, 10);

    // Calculate expiration time
    const lockDuration = price.dynamic_pricing_config?.lock_duration || 180; // Default 180 seconds (3 minutes)
    const expiresAt = Math.floor(Date.now() / 1000) + lockDuration;

    const slippagePercent = normalizeSlippagePercent(slippagePercentInput);

    // Calculate max_payable_token
    // Priority: min_acceptable_rate > slippage_percent
    let maxPayableToken: string | undefined;
    if (minAcceptableRate && Number(minAcceptableRate) > 0) {
      // Use min_acceptable_rate: max_payable_token = base_amount / min_acceptable_rate
      const minRateBN = fromTokenToUnit(trimDecimals(minAcceptableRate, USD_DECIMALS), USD_DECIMALS);
      const maxPayableRaw = totalBaseAmountBN
        .mul(new BN(10).pow(new BN(currency.decimal)))
        .add(minRateBN)
        .sub(new BN(1))
        .div(minRateBN);
      maxPayableToken = limitTokenPrecision(maxPayableRaw, currency.decimal, 10).toString();
    } else {
      // Use slippage_percent: max_payable_token = quoted_amount * (1 + slippage_percent/100)
      const SLIPPAGE_BPS_BASE = new BN(10000);
      const slippageBps = Math.round(slippagePercent * 100);
      const multiplier = SLIPPAGE_BPS_BASE.add(new BN(slippageBps));
      maxPayableToken = quotedAmount
        .mul(multiplier)
        .add(SLIPPAGE_BPS_BASE.sub(new BN(1)))
        .div(SLIPPAGE_BPS_BASE)
        .toString();
    }

    // Create quote
    const quotePayload = {
      price_id: priceId,
      session_id: sessionId || undefined,
      invoice_id: invoiceId || undefined,
      idempotency_key: idempotencyKey,
      base_currency: price.base_currency,
      base_amount: totalBaseAmountStr,
      target_currency_id: targetCurrencyId,
      rate_currency_symbol: currency.symbol,
      exchange_rate: rateResult.rate,
      quoted_amount: quotedAmount.toString(),
      slippage_percent: slippagePercent,
      max_payable_token: maxPayableToken,
      rate_provider_id: rateResult.provider_id,
      rate_provider_name: rateResult.provider_name,
      rate_timestamp_ms: rateResult.timestamp_ms,
      expires_at: expiresAt,
      status: 'active',
      metadata: {
        calculation: {
          total_base_amount_scaled: totalBaseAmountBN.toString(),
          rate_scaled: rateBN.toString(),
          quoted_amount_unit: quotedAmount.toString(),
        },
        rounding: {
          mode: 'ceil',
          token_decimals: currency.decimal,
        },
        risk: {
          anomaly_detected: false,
          degraded: rateResult.degraded,
          degraded_reason: rateResult.degraded_reason || null,
        },
        rate: {
          consensus_method: resolveConsensusMethod(rateResult),
          providers: rateResult.providers || [],
        },
        slippage: {
          percent: slippagePercent,
          min_acceptable_rate: minAcceptableRate || undefined,
          max_payable_token: maxPayableToken,
        },
        context: {
          quantity,
          base_amount_per_unit: price.base_amount,
        },
      },
    };

    // Create quote with retry logic for idempotency key collision
    const MAX_RETRIES = 3;
    let quote: PriceQuote | null = null;
    let lastError: any = null;

    // Retry loop: sequential await is required for idempotency key collision handling
    // eslint-disable-next-line no-await-in-loop
    for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
      try {
        // eslint-disable-next-line no-await-in-loop
        quote = await PriceQuote.create(quotePayload);
        break; // Success, exit loop
      } catch (error: any) {
        lastError = error;

        if (isUniqueConstraintError(error)) {
          // Try to reuse existing quote with same idempotency key
          // eslint-disable-next-line no-await-in-loop
          const existingByKey = await PriceQuote.findOne({ where: { idempotency_key: quotePayload.idempotency_key } });
          if (existingByKey && existingByKey.canRetry()) {
            logger.warn('Idempotency key collision, reusing existing quote', {
              quoteId: existingByKey.id,
              idempotencyKey: quotePayload.idempotency_key,
              attempt,
              status: existingByKey.status,
            });
            return {
              quote: existingByKey,
              computed_unit_amount: existingByKey.quoted_amount,
              expires_at: existingByKey.expires_at,
            };
          }

          // If last attempt or existing quote not found/not retryable, throw error
          if (attempt >= MAX_RETRIES - 1) {
            logger.error('Max retries exceeded for quote creation in createQuoteWithRate', {
              priceId,
              currencyId: targetCurrencyId,
              attempts: attempt + 1,
              idempotencyKey: quotePayload.idempotency_key,
            });
            break; // Will throw error after loop
          }

          // Generate new idempotency key with retry salt
          const retrySalt = `${idempotencyKeySalt || 'fallback'}:retry:${attempt}:${Date.now()}:${crypto.randomBytes(4).toString('hex')}`;
          quotePayload.idempotency_key = generateIdempotencyKey({
            contextId,
            priceId,
            currencyId: targetCurrencyId,
            quantity,
            baseAmount: totalBaseAmountStr,
            salt: retrySalt,
          });

          logger.info('Retrying quote creation with new idempotency key', {
            priceId,
            attempt: attempt + 1,
            newIdempotencyKey: quotePayload.idempotency_key,
          });
        } else {
          // Non-idempotency error, throw immediately
          throw error;
        }
      }
    }

    // If all retries failed, throw the last error
    if (!quote) {
      throw lastError || new Error('Failed to create quote after retries');
    }

    logger.info('Created new quote with provided rate', {
      quoteId: quote.id,
      priceId,
      currencyId: targetCurrencyId,
      baseAmount: totalBaseAmountStr,
      rate: rateResult.rate,
      quotedAmount: quotedAmount.toString(),
      expiresAt,
    });

    return {
      quote,
      computed_unit_amount: quotedAmount.toString(),
      expires_at: expiresAt,
    };
  }

  /**
   * Refresh an existing quote in place using a pre-fetched exchange rate
   * This is used when quotes expire but we don't want to create a new quote_id
   */
  async refreshQuoteWithRate(
    params: CreateQuoteParams & { rateResult: RateResult; quote_id: string }
  ): Promise<QuoteResponse> {
    const {
      quote_id: quoteId,
      price_id: priceId,
      session_id: sessionId,
      invoice_id: invoiceId,
      target_currency_id: targetCurrencyId,
      quantity,
      rateResult,
      slippage_percent: slippagePercentInput,
    } = params;

    const existing = await PriceQuote.findByPk(quoteId);
    if (!existing) {
      return this.createQuoteWithRate({
        price_id: priceId,
        session_id: sessionId,
        invoice_id: invoiceId,
        target_currency_id: targetCurrencyId,
        quantity,
        rateResult,
      });
    }

    if (existing.status === 'paid' || existing.status === 'used') {
      return {
        quote: existing,
        computed_unit_amount: existing.quoted_amount,
        expires_at: existing.expires_at,
      };
    }
    if (existing.status === 'failed') {
      return this.createQuoteWithRate({
        price_id: priceId,
        session_id: sessionId,
        invoice_id: invoiceId,
        target_currency_id: targetCurrencyId,
        quantity,
        rateResult,
      });
    }

    // Load price
    const price = await Price.findByPk(priceId);
    if (!price) {
      throw new NonRetryableError('PRICE_NOT_FOUND', `Price ${priceId} not found`);
    }

    if (price.pricing_type !== 'dynamic') {
      throw new NonRetryableError('INVALID_PRICE_TYPE', `Price ${priceId} is not a dynamic price`);
    }

    if (!price.base_currency || !price.base_amount) {
      throw new NonRetryableError('INVALID_PRICE_CONFIG', `Price ${priceId} missing base_currency or base_amount`);
    }

    // Load target currency
    const currency = await PaymentCurrency.findByPk(targetCurrencyId);
    if (!currency) {
      throw new NonRetryableError('CURRENCY_NOT_FOUND', `Currency ${targetCurrencyId} not found`);
    }

    // Calculate total base amount using BN for precision
    const USD_DECIMALS = 8;
    const baseAmountBN = fromTokenToUnit(trimDecimals(price.base_amount, USD_DECIMALS), USD_DECIMALS);
    const quantityBN = new BN(quantity);
    const totalBaseAmountBN = baseAmountBN.mul(quantityBN);
    const totalBaseAmountStr = fromUnitToToken(totalBaseAmountBN.toString(), USD_DECIMALS);

    // Calculate quoted amount using pure BN arithmetic
    const rateBN = fromTokenToUnit(trimDecimals(rateResult.rate, USD_DECIMALS), USD_DECIMALS);
    const numerator = totalBaseAmountBN.mul(new BN(10).pow(new BN(currency.decimal)));
    const denominator = rateBN;
    const quotedAmountRaw = numerator.add(denominator).sub(new BN(1)).div(denominator);
    const quotedAmount = limitTokenPrecision(quotedAmountRaw, currency.decimal, 10);

    // Calculate expiration time
    const lockDuration = price.dynamic_pricing_config?.lock_duration || 180; // Default 180 seconds (3 minutes)
    const expiresAt = Math.floor(Date.now() / 1000) + lockDuration;

    const slippagePercent = normalizeSlippagePercent(slippagePercentInput);

    await existing.update({
      base_currency: price.base_currency,
      base_amount: totalBaseAmountStr,
      target_currency_id: targetCurrencyId,
      rate_currency_symbol: currency.symbol,
      exchange_rate: rateResult.rate,
      quoted_amount: quotedAmount.toString(),
      slippage_percent: slippagePercent,
      rate_provider_id: rateResult.provider_id,
      rate_provider_name: rateResult.provider_name,
      rate_timestamp_ms: rateResult.timestamp_ms,
      expires_at: expiresAt,
      status: 'active',
      metadata: {
        calculation: {
          total_base_amount_scaled: totalBaseAmountBN.toString(),
          rate_scaled: rateBN.toString(),
          quoted_amount_unit: quotedAmount.toString(),
        },
        rounding: {
          mode: 'ceil',
          token_decimals: currency.decimal,
        },
        risk: {
          anomaly_detected: false,
          degraded: rateResult.degraded,
          degraded_reason: rateResult.degraded_reason || null,
        },
        rate: {
          consensus_method: resolveConsensusMethod(rateResult),
          providers: rateResult.providers || [],
        },
        slippage: {
          percent: slippagePercent,
        },
        context: {
          quantity,
          base_amount_per_unit: price.base_amount,
        },
      } as QuoteMetadata,
    });

    return {
      quote: existing,
      computed_unit_amount: existing.quoted_amount,
      expires_at: existing.expires_at,
    };
  }

  /**
   * @deprecated In Final Freeze, Quotes are created as 'used' at Submit.
   * This method is kept for backward compatibility.
   * Use isUsable() or canRetry() to check Quote validity.
   */
  async validateAndConsume(quoteId: string, transaction?: any): Promise<PriceQuote> {
    // Use FOR UPDATE to lock the row
    const quote = await PriceQuote.findOne({
      where: { id: quoteId },
      lock: transaction ? transaction.LOCK.UPDATE : undefined,
      transaction,
    });

    if (!quote) {
      throw new NonRetryableError('QUOTE_NOT_FOUND', `Quote ${quoteId} not found`);
    }

    // Final Freeze: Check if quote can be used (used or payment_failed)
    if (!quote.canRetry()) {
      throw new NonRetryableError('QUOTE_INVALID', `Quote ${quoteId} cannot be used (status: ${quote.status})`);
    }

    // In Final Freeze, quote is already 'used' - just return it
    return quote;
  }

  /**
   * Mark quote as paid
   * Idempotent: if quote not found or already paid, logs and returns silently
   */
  async markAsPaid(quoteId: string, transaction?: any): Promise<void> {
    const quote = await PriceQuote.findByPk(quoteId, { transaction });
    if (!quote) {
      logger.warn('Quote not found when marking as paid', { quoteId });
      return;
    }

    await quote.markAsPaid(transaction);
  }

  /**
   * Mark all quotes associated with an invoice as paid
   * Idempotent: already paid quotes are skipped silently
   */
  async markQuotesAsPaidByInvoice(invoiceId: string, transaction?: any): Promise<string[]> {
    const quotes = await PriceQuote.findAll({
      where: { invoice_id: invoiceId },
      transaction,
    });

    if (quotes.length === 0) {
      return [];
    }

    const markedIds: string[] = [];
    await Promise.all(
      quotes.map(async (quote) => {
        if (quote.status !== 'paid') {
          await quote.markAsPaid(transaction);
          markedIds.push(quote.id);
        }
      })
    );

    if (markedIds.length > 0) {
      logger.info('Marked quotes as paid by invoice', { invoiceId, quoteIds: markedIds });
    }

    return markedIds;
  }

  /**
   * @deprecated In Final Freeze, Quotes don't expire by time.
   * This method is a no-op for backward compatibility.
   */
  expireOldQuotes(): number {
    // Final Freeze: Quotes don't expire by time, this is a no-op
    logger.info('expireOldQuotes called but quotes no longer expire (Final Freeze)');
    return 0;
  }

  /**
   * Create Quote at Submit time with dual-layer validation (Final Freeze Architecture)
   *
   * This is the ONLY way to create Quotes in the new architecture.
   * Quote is created directly as 'used' status.
   *
   * Validation layers:
   * 1. System-level: Rate trustworthiness (non-bypassable)
   * 2. User-level: Slippage protection (bypassable with confirmation)
   *
   * @see Intent: blocklets/core/ai/intent/20260112-dynamic-price.md
   */
  async createUsedQuoteWithValidation(params: {
    price_id: string;
    session_id?: string;
    invoice_id?: string;
    idempotency_key: string;
    target_currency_id: string;
    quantity: number;
    preview_rate?: string;
    slippage_percent?: number;
    price_confirmed?: boolean;
  }): Promise<{
    quote: PriceQuote;
    validation_passed: boolean;
    error_code?: 'PRICE_UNAVAILABLE' | 'PRICE_UNSTABLE' | 'PRICE_CHANGED';
    error_message?: string;
    change_percent?: number;
  }> {
    const {
      price_id: priceId,
      session_id: sessionId,
      invoice_id: invoiceId,
      idempotency_key: idempotencyKey,
      target_currency_id: targetCurrencyId,
      quantity,
      preview_rate: previewRate,
      slippage_percent: slippagePercentInput,
      price_confirmed: priceConfirmed = false,
    } = params;

    // Check idempotency: return existing quote if found
    const existingQuote = await PriceQuote.findByIdempotencyKey(idempotencyKey);
    if (existingQuote) {
      logger.info('Idempotent submit: returning existing quote', {
        quoteId: existingQuote.id,
        idempotencyKey,
        status: existingQuote.status,
      });
      return {
        quote: existingQuote,
        validation_passed: true,
      };
    }

    // Load price
    const price = await Price.findByPk(priceId);
    if (!price) {
      throw new NonRetryableError('PRICE_NOT_FOUND', `Price ${priceId} not found`);
    }

    if (price.pricing_type !== 'dynamic') {
      throw new NonRetryableError('INVALID_PRICE_TYPE', `Price ${priceId} is not a dynamic price`);
    }

    if (!price.base_currency || !price.base_amount) {
      throw new NonRetryableError('INVALID_PRICE_CONFIG', `Price ${priceId} missing base_currency or base_amount`);
    }

    // Load target currency
    const currency = (await PaymentCurrency.findByPk(targetCurrencyId, {
      include: [{ model: PaymentMethod, as: 'payment_method' }],
    })) as PaymentCurrency & { payment_method: PaymentMethod };

    if (!currency) {
      throw new NonRetryableError('CURRENCY_NOT_FOUND', `Currency ${targetCurrencyId} not found`);
    }

    if (currency.payment_method?.type === 'stripe') {
      throw new NonRetryableError(
        'CURRENCY_NOT_SUPPORTED',
        `Currency ${targetCurrencyId} is not supported for dynamic pricing`
      );
    }

    // Fetch current exchange rate
    const exchangeRateService = getExchangeRateService();
    const rateSymbol = getExchangeRateSymbol(currency.symbol, currency.payment_method?.type as ChainType);

    let rateResult;
    try {
      rateResult = await exchangeRateService.getRate(rateSymbol);
    } catch (error: any) {
      logger.error('Failed to fetch exchange rate for submit', {
        priceId,
        currencyId: targetCurrencyId,
        error: error.message,
      });
      // Return validation failure with PRICE_UNAVAILABLE
      return {
        quote: null as any,
        validation_passed: false,
        error_code: 'PRICE_UNAVAILABLE',
        error_message: 'Unable to fetch exchange rate. Please try again later.',
      };
    }

    // Dual-layer validation
    const slippagePercent = normalizeSlippagePercent(slippagePercentInput);
    const validationResult = validateForSubmit(
      {
        rate: rateResult.rate,
        timestamp_ms: rateResult.timestamp_ms,
        provider_id: rateResult.provider_id,
        provider_name: rateResult.provider_name,
        degraded: rateResult.degraded,
        degraded_reason: rateResult.degraded_reason,
        providers: rateResult.providers,
        consensus_method: rateResult.consensus_method,
      },
      rateSymbol,
      previewRate,
      slippagePercent,
      priceConfirmed
    );

    if (!validationResult.passed) {
      logger.info('Submit validation failed', {
        priceId,
        currencyId: targetCurrencyId,
        errorCode: validationResult.error_code,
        errorMessage: validationResult.error_message,
        changePercent: validationResult.slippage_validation?.change_percent,
      });

      return {
        quote: null as any,
        validation_passed: false,
        error_code: validationResult.error_code,
        error_message: validationResult.error_message,
        change_percent: validationResult.slippage_validation?.change_percent,
      };
    }

    // Calculate quoted amount
    const USD_DECIMALS = 8;
    const baseAmountBN = fromTokenToUnit(trimDecimals(price.base_amount, USD_DECIMALS), USD_DECIMALS);
    const quantityBN = new BN(quantity);
    const totalBaseAmountBN = baseAmountBN.mul(quantityBN);
    const totalBaseAmountStr = fromUnitToToken(totalBaseAmountBN.toString(), USD_DECIMALS);

    const rateBN = fromTokenToUnit(trimDecimals(rateResult.rate, USD_DECIMALS), USD_DECIMALS);
    const numerator = totalBaseAmountBN.mul(new BN(10).pow(new BN(currency.decimal)));
    const denominator = rateBN;
    const quotedAmountRaw = numerator.add(denominator).sub(new BN(1)).div(denominator);
    const quotedAmount = limitTokenPrecision(quotedAmountRaw, currency.decimal, 10);

    // Calculate slippage fields using basis points (万分位) for precision
    // slippagePercent = 0.5 means 0.5%, which is 50 basis points
    const SLIPPAGE_BPS_BASE = 10000;
    const slippageBps = Math.round(slippagePercent * 100); // 0.5% -> 50 bps
    const maxPayableToken = quotedAmount
      .mul(new BN(SLIPPAGE_BPS_BASE + slippageBps))
      .add(new BN(SLIPPAGE_BPS_BASE - 1)) // 向上取整
      .div(new BN(SLIPPAGE_BPS_BASE));
    const minAcceptableRate = rateBN.mul(new BN(SLIPPAGE_BPS_BASE - slippageBps)).div(new BN(SLIPPAGE_BPS_BASE));

    // Create Quote directly as 'used' (Final Freeze)
    const quoteInput: CreateQuoteInput = {
      price_id: priceId,
      session_id: sessionId,
      invoice_id: invoiceId,
      idempotency_key: idempotencyKey,
      base_currency: price.base_currency,
      base_amount: totalBaseAmountStr,
      target_currency_id: targetCurrencyId,
      rate_currency_symbol: currency.symbol,
      exchange_rate: rateResult.rate,
      quoted_amount: quotedAmount.toString(),
      rate_provider_id: rateResult.provider_id,
      rate_provider_name: rateResult.provider_name,
      rate_timestamp_ms: rateResult.timestamp_ms,
      slippage_percent: slippagePercent,
      max_payable_token: maxPayableToken.toString(),
      min_acceptable_rate: fromUnitToToken(minAcceptableRate.toString(), USD_DECIMALS),
      metadata: {
        calculation: {
          total_base_amount_scaled: totalBaseAmountBN.toString(),
          rate_scaled: rateBN.toString(),
          quoted_amount_unit: quotedAmount.toString(),
        },
        rounding: {
          mode: 'ceil',
          token_decimals: currency.decimal,
        },
        risk: {
          anomaly_detected: false,
          degraded: rateResult.degraded,
          degraded_reason: rateResult.degraded_reason || null,
        },
        rate: {
          consensus_method: resolveConsensusMethod(rateResult),
          providers: rateResult.providers || [],
        },
        slippage: {
          percent: slippagePercent,
          max_payable_token: maxPayableToken.toString(),
          min_acceptable_rate: fromUnitToToken(minAcceptableRate.toString(), USD_DECIMALS),
          derived_at_ms: Date.now(),
        },
        context: {
          quantity,
          base_amount_per_unit: price.base_amount,
          preview_rate: previewRate,
          price_confirmed: priceConfirmed,
        },
      } as QuoteMetadata,
    };

    const quote = await PriceQuote.createUsedQuote(quoteInput);

    logger.info('Created used quote at submit (Final Freeze)', {
      quoteId: quote.id,
      priceId,
      currencyId: targetCurrencyId,
      baseAmount: totalBaseAmountStr,
      rate: rateResult.rate,
      quotedAmount: quotedAmount.toString(),
      status: quote.status,
      idempotencyKey,
    });

    return {
      quote,
      validation_passed: true,
    };
  }

  /**
   * Mark quote as payment_failed (Final Freeze)
   */
  async markAsPaymentFailed(quoteId: string, transaction?: any): Promise<void> {
    const quote = await PriceQuote.findByPk(quoteId, { transaction });
    if (!quote) {
      throw new Error(`Quote ${quoteId} not found`);
    }

    await quote.markAsPaymentFailed(transaction);
  }
}

// Singleton instance
let serviceInstance: QuoteService | null = null;

export function getQuoteService(): QuoteService {
  if (!serviceInstance) {
    serviceInstance = new QuoteService();
  }
  return serviceInstance;
}
