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

import { PriceQuote } from '../store/models/price-quote';
import { InvoiceItem, Invoice, PaymentIntent, CheckoutSession } from '../store/models';
import logger from './logger';

export interface QuoteValidationResult {
  valid: boolean;
  quoteId?: string;
  reason?: string;
  action: 'allow' | 'require_manual_review' | 'reject';
}

const SLIPPAGE_BPS_BASE = new BN(10000);

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

const resolveQuoteMaxPayableToken = (quote: PriceQuote): string | null => {
  const direct = quote.max_payable_token ?? (quote.metadata as any)?.slippage?.max_payable_token;
  if (direct) {
    return direct;
  }
  if (!quote.quoted_amount) {
    return null;
  }
  const slippagePercent = normalizeSlippagePercent(
    quote.slippage_percent ?? (quote.metadata as any)?.slippage?.percent
  );
  if (slippagePercent === null) {
    return quote.quoted_amount;
  }
  const slippageBps = Math.round(slippagePercent * 100);
  const multiplier = SLIPPAGE_BPS_BASE.add(new BN(slippageBps));
  const quotedAmountUnit = new BN(quote.quoted_amount);
  return quotedAmountUnit
    .mul(multiplier)
    .add(SLIPPAGE_BPS_BASE.sub(new BN(1)))
    .div(SLIPPAGE_BPS_BASE)
    .toString();
};

const resolveMaxPayableTotal = async (params: { invoiceId?: string; quotes: PriceQuote[] }): Promise<BN | null> => {
  const { invoiceId, quotes } = params;
  if (!invoiceId || quotes.length === 0) {
    if (quotes.length === 0) {
      return null;
    }
    return quotes.reduce((sum, quote) => {
      const maxPayable = resolveQuoteMaxPayableToken(quote) || quote.quoted_amount || '0';
      return sum.add(new BN(maxPayable));
    }, new BN(0));
  }

  const invoiceItems = await InvoiceItem.findAll({ where: { invoice_id: invoiceId } });
  if (!invoiceItems.length) {
    return null;
  }

  const quotesById = new Map(quotes.map((quote) => [quote.id, quote]));

  // Check if ANY invoice items have quote_id linked
  const itemsWithQuoteId = invoiceItems.filter((item) => item.metadata?.quote_id);

  // If no invoice items have quote_id but quotes exist (linked via invoice_id),
  // use quotes' max_payable_token directly instead of falling back to item amounts.
  // This handles the case where quotes were created for dynamic pricing but
  // invoice items weren't updated with quote_id (e.g., auto-recharge invoices before the fix).
  if (itemsWithQuoteId.length === 0 && quotes.length > 0) {
    logger.info('No invoice items have quote_id, using quotes max_payable_token directly', {
      invoiceId,
      quoteIds: quotes.map((q) => q.id),
      itemCount: invoiceItems.length,
    });
    return quotes.reduce((sum, quote) => {
      const maxPayable = resolveQuoteMaxPayableToken(quote) || quote.quoted_amount || '0';
      return sum.add(new BN(maxPayable));
    }, new BN(0));
  }

  // Normal case: match invoice items to quotes by quote_id
  let total = new BN(0);
  invoiceItems.forEach((item) => {
    const quoteId = item.metadata?.quote_id;
    const quote = quoteId ? quotesById.get(quoteId) : undefined;
    if (quote) {
      const maxPayable = resolveQuoteMaxPayableToken(quote) || quote.quoted_amount || '0';
      total = total.add(new BN(maxPayable));
      return;
    }
    if (item.amount) {
      total = total.add(new BN(item.amount));
    }
  });
  return total;
};

/**
 * Validate quote status before processing on-chain payment
 * This function ensures that payments are only processed for active quotes
 *
 * P0 CRITICAL: This prevents silent success when users pay after quote expiration
 *
 * @param checkoutSessionId - Optional checkout session ID
 * @param invoiceId - Optional invoice ID
 * @returns Validation result with recommended action
 */
export async function validateQuoteForPayment(params: {
  checkoutSessionId?: string;
  invoiceId?: string;
  amount?: string;
  skipMaxPayableCheck?: boolean;
}): Promise<QuoteValidationResult> {
  const { checkoutSessionId, invoiceId, amount, skipMaxPayableCheck = false } = params;

  // Get quotes associated with this payment
  const quotes = await findRelatedQuotes({ checkoutSessionId, invoiceId });

  if (quotes.length === 0) {
    // No quotes found - this is a fixed-price payment, allow it
    return {
      valid: true,
      action: 'allow',
    };
  }

  // Check all quotes
  // Final Freeze: Quote statuses are 'used', 'paid', 'payment_failed'
  // Quotes no longer expire by time - they only expire when flow ends
  const usedQuotes: PriceQuote[] = [];
  const paidQuotes: PriceQuote[] = [];
  const failedQuotes: PriceQuote[] = [];

  for (const quote of quotes) {
    if (quote.status === 'paid') {
      paidQuotes.push(quote);
    } else if (quote.status === 'used') {
      // 'used' quotes are valid for payment - this is the normal state after submit
      usedQuotes.push(quote);
    } else if (quote.status === 'payment_failed') {
      // 'payment_failed' quotes can be retried
      failedQuotes.push(quote);
    }
    // Note: 'active' and 'expired' statuses are deprecated in Final Freeze
  }

  // Case 1: All quotes are already paid - check if this is a legitimate duplicate or needs special handling
  if (paidQuotes.length === quotes.length && paidQuotes.length > 0) {
    // Check if the checkout session has already completed successfully
    // If so, this is an idempotent request and we should return success instead of reject
    if (checkoutSessionId) {
      const checkoutSession = await CheckoutSession.findByPk(checkoutSessionId);
      if (checkoutSession?.status === 'complete' && checkoutSession?.payment_status === 'paid') {
        logger.info('All quotes already paid - checkout session already completed (idempotent success)', {
          checkoutSessionId,
          invoiceId,
          quoteIds: paidQuotes.map((q) => q.id),
        });
        return {
          valid: true,
          quoteId: paidQuotes[0]?.id,
          reason: 'Already completed - idempotent success',
          action: 'allow',
        };
      }
    }

    logger.warn('All quotes already paid - duplicate payment attempt', {
      checkoutSessionId,
      invoiceId,
      quoteIds: paidQuotes.map((q) => q.id),
    });
    return {
      valid: false,
      quoteId: paidQuotes[0]?.id,
      reason: 'All quotes already paid - duplicate payment',
      action: 'reject',
    };
  }

  if (!skipMaxPayableCheck && amount && quotes.length > 0) {
    const maxPayableTotal = await resolveMaxPayableTotal({ invoiceId, quotes });
    if (maxPayableTotal) {
      const amountBN = new BN(amount);
      if (amountBN.gt(maxPayableTotal)) {
        logger.error('Payment exceeds max payable token limit', {
          checkoutSessionId,
          invoiceId,
          amount,
          maxPayableTotal: maxPayableTotal.toString(),
          quoteIds: quotes.map((q) => q.id),
        });
        return {
          valid: false,
          reason: `QUOTE_MAX_PAYABLE_EXCEEDED: amount ${amount} > max ${maxPayableTotal.toString()}`,
          action: 'reject',
        };
      }
    }
  }

  // Case 2: All quotes are already used/paid - need to check if this is a duplicate payment
  if (usedQuotes.length === quotes.length) {
    // Check if this is a legitimate payment (quotes consumed during submit, but payment not yet completed)
    // or a duplicate payment (payment already succeeded)
    let isLegitimatePayment = true;

    if (invoiceId) {
      const invoice = await Invoice.findByPk(invoiceId);

      // If invoice is already paid, this is a duplicate payment
      if (invoice?.status === 'paid') {
        isLegitimatePayment = false;
      } else if (invoice?.payment_intent_id) {
        // Check payment intent status
        const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
        // If payment intent already succeeded, this is a duplicate payment
        if (paymentIntent?.status === 'succeeded') {
          isLegitimatePayment = false;
        }
      }
    }

    if (!isLegitimatePayment) {
      logger.warn('All quotes already used and payment already completed - duplicate payment', {
        checkoutSessionId,
        invoiceId,
        quoteIds: usedQuotes.map((q) => q.id),
      });

      return {
        valid: false,
        reason: 'All associated quotes have already been used and payment completed',
        action: 'reject',
      };
    }

    // Quotes are 'used' but payment not yet completed - this is the normal submit → pay flow
    logger.info('Quotes are used but payment not completed - allowing payment', {
      checkoutSessionId,
      invoiceId,
      quoteIds: usedQuotes.map((q) => q.id),
    });

    // Allow the payment to proceed with the used quotes
    return {
      valid: true,
      quoteId: usedQuotes[0]?.id,
      action: 'allow',
    };
  }

  // Case 3: Some quotes are 'used' and some are 'payment_failed' - allow retry
  if (usedQuotes.length > 0 || failedQuotes.length > 0) {
    const validQuotes = [...usedQuotes, ...failedQuotes];
    logger.info('Payment allowed with used/failed quotes', {
      checkoutSessionId,
      invoiceId,
      usedCount: usedQuotes.length,
      failedCount: failedQuotes.length,
      quoteIds: validQuotes.map((q) => q.id),
    });
    return {
      valid: true,
      quoteId: validQuotes[0]?.id,
      action: 'allow',
    };
  }

  // Case 4: Unknown state - log and allow (fail-open for edge cases)
  logger.warn('Unknown quote state detected - allowing payment', {
    checkoutSessionId,
    invoiceId,
    quoteCount: quotes.length,
    quoteStatuses: quotes.map((q) => ({ id: q.id, status: q.status })),
  });

  return {
    valid: true,
    reason: 'Unknown state - allowed by default',
    action: 'allow',
  };
}

/**
 * Find all quotes related to a checkout session or invoice
 */
async function findRelatedQuotes(params: { checkoutSessionId?: string; invoiceId?: string }): Promise<PriceQuote[]> {
  const { checkoutSessionId, invoiceId } = params;
  const quotes: PriceQuote[] = [];

  if (checkoutSessionId) {
    // Find quotes associated with checkout session
    const sessionQuotes = await PriceQuote.findAll({
      where: { session_id: checkoutSessionId },
    });
    quotes.push(...sessionQuotes);
  }

  if (invoiceId) {
    // Find quotes associated with invoice
    const invoiceQuotes = await PriceQuote.findAll({
      where: { invoice_id: invoiceId },
    });
    quotes.push(...invoiceQuotes);

    // Also check invoice items metadata for quote references
    const invoiceItems = await InvoiceItem.findAll({
      where: { invoice_id: invoiceId },
    });

    // Sequential DB queries needed for quote lookup - parallel would be less efficient for small sets
    // eslint-disable-next-line no-await-in-loop
    for (const item of invoiceItems) {
      const quoteId = item.metadata?.quote_id;
      if (quoteId && typeof quoteId === 'string') {
        // eslint-disable-next-line no-await-in-loop
        const quote = await PriceQuote.findByPk(quoteId);
        if (quote) {
          quotes.push(quote);
        }
      }
    }
  }

  // Deduplicate quotes
  const uniqueQuotes = Array.from(new Map(quotes.map((q) => [q.id, q])).values());

  return uniqueQuotes;
}

/**
 * Handle payment with expired quote
 * This function should be called when a payment arrives after quote expiration
 *
 * P0 CRITICAL: This ensures funds are not lost and proper procedures are followed
 *
 * @param params - Payment details
 * @returns Action taken
 */
export function handleExpiredQuotePayment(params: {
  quoteId: string;
  checkoutSessionId?: string;
  invoiceId?: string;
  paymentIntentId?: string;
  amount: string;
  currencyId: string;
  txHash?: string;
}): {
  action: 'held_for_review' | 'refund_initiated';
  message: string;
} {
  const { quoteId, checkoutSessionId, invoiceId, paymentIntentId, amount, currencyId, txHash } = params;

  logger.error('Payment received with expired quote - initiating review process', {
    quoteId,
    checkoutSessionId,
    invoiceId,
    paymentIntentId,
    amount,
    currencyId,
    txHash,
    timestamp: new Date().toISOString(),
  });

  // TODO: Integrate with your manual review / dispute resolution system
  // Options:
  // 1. Hold payment for manual review
  // 2. Automatically refund
  // 3. Create a new quote at current rate and ask customer to confirm
  // 4. Create support ticket

  // For now, mark payment as requiring manual review
  // This prevents silent success and ensures proper handling

  return {
    action: 'held_for_review',
    message: `Payment held for manual review due to expired quote. Quote ID: ${quoteId}`,
  };
}
