/**
 * Payments Service
 * Business logic for payment processing
 */

// Import Payment and related models
const { Payment, Sale, User } = require('../../../models');
// Import app configuration
const appConfig = require('../../../config/app');
// Import inventory finalization service
const { finalizeInventoryForSale } = require('../../sales/services/inventoryFinalization');
// Import custom error classes
const { NotFoundError, ValidationError } = require('../../../utils/errors');
// Import logger for logging
const logger = require('../../../utils/logger');
// Import Sequelize operators and transaction
const { Op } = require('sequelize');
const { sequelize } = require('../../../models');
// Import axios for HTTP requests (Paystack API)
const axios = require('axios');
// Import crypto for generating payment references
const crypto = require('crypto');
// Import M-Pesa service
const mpesaService = require('./mpesa');

/**
 * Generate payment reference
 * Generates unique payment reference for Paystack/card payments
 * @returns {string} Generated payment reference
 */
const generatePaymentReference = () => {
  // Generate random reference using crypto
  const randomBytes = crypto.randomBytes(16); // Generate 16 random bytes
  const reference = `PAY-${randomBytes.toString('hex').toUpperCase()}`; // Format: PAY-XXXXXXXX
  return reference; // Return reference
};

/**
 * Calculate remaining balance on sale
 * Calculates how much is still owed on a sale
 * @param {number} saleId - Sale ID
 * @returns {Promise<number>} Remaining balance
 */
const calculateRemainingBalance = async (saleId, transaction = null) => {
  // Get sale (within transaction if provided)
  const sale = await Sale.findByPk(saleId, { transaction }); // Find sale by ID
  if (!sale) {
    throw new NotFoundError('Sale not found'); // Throw error if sale doesn't exist
  }
  
  // Get all successful payments for this sale (within transaction if provided)
  const payments = await Payment.findAll({
    where: {
      sale_id: saleId, // Match sale ID
      status: 'SUCCESS', // Only count successful payments
    },
    transaction, // Use transaction if provided (allows seeing payments created in same transaction)
  });
  
  // Calculate total paid amount
  const totalPaid = payments.reduce((sum, payment) => {
    return sum + parseFloat(payment.amount || 0);
  }, 0); // Sum payment amounts
  
  // Calculate remaining balance (round to 2 decimal places to avoid floating point issues)
  const saleTotal = parseFloat(sale.total || 0);
  const totalPaidRounded = Math.round(totalPaid * 100) / 100; // Round to 2 decimals
  const saleTotalRounded = Math.round(saleTotal * 100) / 100; // Round to 2 decimals
  const remainingBalance = saleTotalRounded - totalPaidRounded; // Total - paid
  
  // Return remaining balance (cannot be negative, round to 2 decimals)
  return Math.max(0, Math.round(remainingBalance * 100) / 100); // Return remaining balance (minimum 0, rounded)
};

/**
 * Check if sale is fully paid
 * Checks if sale has been fully paid
 * @param {number} saleId - Sale ID
 * @returns {Promise<boolean>} True if fully paid, false otherwise
 */
const isSaleFullyPaid = async (saleId, transaction = null) => {
  // Calculate remaining balance (pass transaction to see payments created in same transaction)
  const remainingBalance = await calculateRemainingBalance(saleId, transaction); // Get remaining balance
  
  // Return true if remaining balance is 0 or very close to 0 (handle floating point precision)
  // Use a small epsilon to account for floating point rounding errors
  const epsilon = 0.01; // 1 cent tolerance
  return Math.abs(remainingBalance) < epsilon; // Return true if fully paid (or within rounding tolerance)
};

/**
 * Update sale status to PAID if fully paid
 * Updates sale status to PAID when fully paid
 * @param {number} saleId - Sale ID
 * @param {Object} transaction - Sequelize transaction (optional)
 * @returns {Promise<void>}
 */
const updateSaleStatusIfFullyPaid = async (saleId, transaction = null) => {
  try {
    // Get sale first to check current status
    const sale = await Sale.findByPk(saleId, { transaction }); // Find sale in transaction
    if (!sale) {
      logger.error(`Sale ${saleId} not found when checking if fully paid`);
      return;
    }

    // Log current state
    logger.info(`Checking if sale ${saleId} (${sale.invoice_no}) is fully paid. Current status: ${sale.status}, Total: ${sale.total}`);

    // Check if sale is fully paid (pass transaction so it can see payments created in same transaction)
    const fullyPaid = await isSaleFullyPaid(saleId, transaction); // Check if fully paid
    
    // Calculate remaining balance for logging
    const remainingBalance = await calculateRemainingBalance(saleId, transaction);
    
    // Get all payments for this sale to log details
    const payments = await Payment.findAll({
      where: {
        sale_id: saleId,
        status: 'SUCCESS',
      },
      transaction,
    });
    
    const totalPaid = payments.reduce((sum, p) => sum + parseFloat(p.amount || 0), 0);
    
    logger.info(`Sale ${saleId} payment check: Total=${sale.total}, Paid=${totalPaid}, Remaining=${remainingBalance}, FullyPaid=${fullyPaid}`, {
      saleId,
      invoiceNo: sale.invoice_no,
      saleTotal: sale.total,
      totalPaid,
      remainingBalance,
      fullyPaid,
      paymentCount: payments.length,
    });
    
    // If fully paid, update sale status and finalize inventory
    if (fullyPaid) {
      if (sale.status === 'DRAFT') {
        // Update sale status to PAID
        sale.status = 'PAID'; // Set status to PAID
        await sale.save({ transaction }); // Save sale in transaction
        
        // Finalize inventory (mark scanned items as SOLD, auto-mark random items as SOLD)
        try {
          const finalizationResult = await finalizeInventoryForSale(saleId, transaction);
          logger.info(`✅ Sale ${sale.invoice_no} marked as PAID and inventory finalized`, {
            saleId: sale.id,
            invoiceNo: sale.invoice_no,
            saleTotal: sale.total,
            totalPaid,
            markedAsSold: finalizationResult.markedAsSold,
            errors: finalizationResult.errors.length,
          });
          
          if (finalizationResult.errors.length > 0) {
            logger.warn(`Some inventory items could not be finalized for sale ${saleId}:`, {
              errors: finalizationResult.errors,
            });
          }
        } catch (finalizationError) {
          logger.error(`Error finalizing inventory for sale ${saleId}:`, {
            error: finalizationError.message,
            stack: finalizationError.stack,
          });
          // Don't throw error - sale is still marked as PAID, inventory can be fixed manually
        }
        
        // Log sale status update
        logger.info(`✅ Sale ${sale.invoice_no} marked as PAID (fully paid)`, {
          saleId: sale.id,
          invoiceNo: sale.invoice_no,
          saleTotal: sale.total,
          totalPaid,
        });
      } else if (sale.status === 'PAID') {
        // Sale already marked as PAID
        logger.debug(`Sale ${saleId} already marked as PAID`);
      } else {
        // Sale already has a different status
        logger.warn(`Sale ${saleId} status is ${sale.status}, not updating to PAID`, {
          saleId,
          currentStatus: sale.status,
          invoiceNo: sale.invoice_no,
        });
      }
    } else {
      // Log if not fully paid (for debugging)
      logger.warn(`⚠️ Sale ${saleId} not fully paid. Sale total: ${sale.total}, Total paid: ${totalPaid}, Remaining balance: ${remainingBalance}`, {
        saleId,
        invoiceNo: sale.invoice_no,
        saleTotal: sale.total,
        totalPaid,
        remainingBalance,
        paymentCount: payments.length,
      });
    }
  } catch (error) {
    logger.error(`Error updating sale status for sale ${saleId}:`, {
      saleId,
      error: error.message,
      stack: error.stack,
    });
    throw error;
  }
};

/**
 * Initialize Paystack payment
 * Initializes payment with Paystack API
 * @param {number} amount - Payment amount (in kobo for NGN, or smallest currency unit)
 * @param {string} email - Customer email
 * @param {string} reference - Payment reference
 * @param {Object} metadata - Additional metadata (optional)
 * @returns {Promise<Object>} Paystack initialization response
 */
const initializePaystackPayment = async (amount, email, reference, metadata = {}) => {
  // Get Paystack configuration
  const { paystackSecretKey, paystackBaseUrl } = appConfig.payment; // Get Paystack config
  
  // Validate Paystack secret key
  if (!paystackSecretKey) {
    throw new ValidationError('Paystack secret key not configured'); // Throw error if not configured
  }
  
  try {
    // Prepare Paystack API request
    const paystackResponse = await axios.post(
      `${paystackBaseUrl}/transaction/initialize`, // Paystack initialize endpoint
      {
        email, // Customer email
        amount: Math.round(amount * 100), // Convert to kobo (smallest currency unit) - adjust for your currency
        reference, // Payment reference
        metadata, // Additional metadata
      },
      {
        headers: {
          Authorization: `Bearer ${paystackSecretKey}`, // Paystack secret key
          'Content-Type': 'application/json', // Content type
        },
      }
    );
    
    // Return Paystack response
    return paystackResponse.data; // Return Paystack response data
  } catch (error) {
    // Log Paystack error
    logger.error('Paystack initialization error:', {
      error: error.message,
      response: error.response?.data,
    });
    
    // Throw error
    throw new ValidationError(`Paystack payment initialization failed: ${error.response?.data?.message || error.message}`); // Throw error
  }
};

/**
 * Verify Paystack payment
 * Verifies payment status with Paystack API
 * @param {string} reference - Payment reference
 * @returns {Promise<Object>} Paystack verification response
 */
const verifyPaystackPayment = async (reference) => {
  // Get Paystack configuration
  const { paystackSecretKey, paystackBaseUrl } = appConfig.payment; // Get Paystack config
  
  // Validate Paystack secret key
  if (!paystackSecretKey) {
    throw new ValidationError('Paystack secret key not configured'); // Throw error if not configured
  }
  
  try {
    // Verify payment with Paystack
    const paystackResponse = await axios.get(
      `${paystackBaseUrl}/transaction/verify/${reference}`, // Paystack verify endpoint
      {
        headers: {
          Authorization: `Bearer ${paystackSecretKey}`, // Paystack secret key
        },
      }
    );
    
    // Return Paystack response
    return paystackResponse.data; // Return Paystack response data
  } catch (error) {
    // Log Paystack error
    logger.error('Paystack verification error:', {
      error: error.message,
      response: error.response?.data,
    });
    
    // Throw error
    throw new ValidationError(`Paystack payment verification failed: ${error.response?.data?.message || error.message}`); // Throw error
  }
};

/**
 * Create payment
 * Creates a payment record (for cash and other direct payments)
 * @param {Object} paymentData - Payment data (sale_id, provider, amount, reference)
 * @param {number} userId - User ID processing the payment
 * @returns {Promise<Object>} Created payment
 */
const createPayment = async (paymentData, userId) => {
  // Extract payment data
  const { 
    sale_id, 
    provider, 
    amount, 
    reference = null,
    card_reference = null, // Card transaction reference (manual entry)
    mpesa_phone_number = null, // M-Pesa phone number (for STK Push)
  } = paymentData; // Extract payment data
  
  // Validate required fields
  if (!sale_id || !provider || !amount) {
    throw new ValidationError('Sale ID, provider, and amount are required'); // Throw error if missing required fields
  }
  
  // Provider normalization removed - using enum values directly (CASH, CARD, MPESA)
  const normalizedProvider = provider; // Use provider as-is (already validated)
  
  // Validate provider (updated enum: CASH, CARD, MPESA)
  const validProviders = ['CASH', 'CARD', 'MPESA']; // Valid payment providers
  if (!validProviders.includes(provider)) {
    throw new ValidationError(`Provider must be one of: ${validProviders.join(', ')}`); // Throw error if invalid provider
  }
  
  // Validate amount
  if (amount <= 0) {
    throw new ValidationError('Payment amount must be greater than 0'); // Throw error if invalid amount
  }
  
  // Start database transaction
  const transaction = await sequelize.transaction(); // Start transaction
  
  try {
    // Get sale
    const sale = await Sale.findByPk(sale_id, { transaction }); // Find sale in transaction
    if (!sale) {
      throw new NotFoundError('Sale not found'); // Throw error if sale doesn't exist
    }
    
    // Check if sale can accept payments (not cancelled)
    if (sale.status === 'CANCELLED') {
      throw new ValidationError('Cannot process payment for cancelled sale'); // Throw error if sale cancelled
    }
    
    // Calculate remaining balance (within transaction to see existing payments)
    const remainingBalance = await calculateRemainingBalance(sale_id, transaction); // Get remaining balance
    
    // Round amount to 2 decimal places for comparison (handle floating point precision)
    const roundedAmount = Math.round(parseFloat(amount) * 100) / 100;
    
    // Validate payment amount doesn't exceed remaining balance (with small tolerance for floating point)
    const epsilon = 0.01; // 1 cent tolerance
    if (roundedAmount > remainingBalance + epsilon) {
      throw new ValidationError(`Payment amount (${roundedAmount}) exceeds remaining balance (${remainingBalance})`); // Throw error if amount exceeds balance
    }
    
    // Use rounded amount for payment creation
    const paymentAmount = roundedAmount;
    
    // Determine tender_number (1 or 2) - check existing payments for this sale
    const existingPayments = await Payment.findAll({
      where: {
        sale_id: sale_id, // Match sale ID
      },
      transaction, // Use transaction
    });
    
    // Calculate next tender number (max existing tender_number + 1, max 2)
    const maxTenderNumber = existingPayments.length > 0
      ? Math.max(...existingPayments.map(p => p.tender_number || 1))
      : 0;
    const tenderNumber = Math.min(maxTenderNumber + 1, 2); // Max 2 tenders
    
    // Validate we haven't exceeded 2 tenders
    if (existingPayments.length >= 2) {
      throw new ValidationError('Maximum of 2 payments per sale is allowed'); // Throw error if too many payments
    }
    
    // Generate reference if not provided (for CARD/MPESA payments)
    let paymentReference = reference; // Use provided reference
    if (!paymentReference && normalizedProvider !== 'CASH') {
      paymentReference = generatePaymentReference(); // Generate reference for CARD/MPESA payments
    }
    
    // Determine payment status based on provider
    let paymentStatus = 'PENDING'; // Default status
    let paidAt = null; // Default paid date
    
    // Payment status handling (CASH is immediate, CARD/MPESA may be pending)
    if (normalizedProvider === 'CASH') {
      paymentStatus = 'SUCCESS'; // Set status to SUCCESS for cash payments
      paidAt = new Date(); // Set paid date to now
    }
    // CARD and MPESA payments may remain PENDING until webhook confirms success
    
    // Create payment record with tender_number and additional fields
    // Use paymentAmount (rounded) instead of raw amount to ensure consistency
    const payment = await Payment.create({
      sale_id, // Link to sale
      provider: normalizedProvider, // Payment provider (CASH, CARD, MPESA)
      reference: paymentReference, // Payment reference
      amount: paymentAmount, // Payment amount (rounded to 2 decimals)
      status: paymentStatus, // Payment status
      paid_at: paidAt, // Paid date (if successful)
      tender_number: tenderNumber, // Tender sequence number (1 or 2)
      card_reference: card_reference || null, // Card transaction reference (for card payments)
      mpesa_phone_number: mpesa_phone_number || null, // M-Pesa phone number (for M-Pesa payments)
    }, { transaction }); // Create payment in transaction
    
    // Update sale status if fully paid
    await updateSaleStatusIfFullyPaid(sale_id, transaction); // Update sale status
    
    // For MPESA payments, initiate STK Push
    let stkPushResult = null;
    if (normalizedProvider === 'MPESA' && mpesa_phone_number) {
      try {
        // Format phone number to E.164 format (M-Pesa service handles this)
        // Initiate STK Push
        const stkResult = await mpesaService.initiateSTKPush({
          phoneNumber: mpesa_phone_number, // M-Pesa service will format it
          amount,
          accountReference: sale.invoice_no || `SALE-${sale_id}`,
          transactionDesc: `Payment for sale ${sale.invoice_no || sale_id}`,
        });
        
        // Update payment reference with checkout request ID
        payment.reference = stkResult.checkoutRequestID;
        await payment.save({ transaction });
        
        stkPushResult = {
          checkoutRequestID: stkResult.checkoutRequestID,
          customerMessage: stkResult.customerMessage,
        };
        
        logger.info('M-Pesa STK Push initiated via createPayment', {
          paymentId: payment.id,
          saleId: sale_id,
          checkoutRequestID: stkResult.checkoutRequestID,
          phoneNumber: mpesa_phone_number,
          amount,
        });
      } catch (stkError) {
        // Log STK Push error but don't fail the payment creation
        logger.error('Failed to initiate STK Push', {
          paymentId: payment.id,
          saleId: sale_id,
          error: stkError.message,
        });
        // Set a flag to indicate STK Push failed
        stkPushResult = {
          error: true,
          message: stkError.message || 'Failed to initiate STK Push',
          // Indicate if it's a configuration issue
          configurationError: stkError.message.includes('not configured'),
        };
        // Continue without STK Push - payment will remain PENDING
      }
    }
    
    // Commit transaction
    await transaction.commit(); // Commit transaction
    
    // Reload payment with associations (sale should have updated status)
    const paymentWithDetails = await Payment.findByPk(payment.id, {
      include: [
        {
          model: Sale, // Include sale details
          as: 'sale', // Use sale alias
          attributes: ['id', 'invoice_no', 'total', 'status', 'subtotal', 'discount_amount'], // Select specific attributes
        },
      ],
    });
    
    // Log final sale status for debugging
    if (paymentWithDetails.sale) {
      logger.info(`Payment ${payment.id} created. Final sale status: ${paymentWithDetails.sale.status}`, {
        paymentId: payment.id,
        saleId: payment.sale_id,
        saleStatus: paymentWithDetails.sale.status,
        saleTotal: paymentWithDetails.sale.total,
        paymentAmount: payment.amount,
        provider: normalizedProvider,
      });
    }
    
    // Log payment creation
    logger.info(`Payment created: ${paymentReference || 'CASH'}`, {
      paymentId: payment.id,
      saleId: sale_id,
      provider: normalizedProvider, // Log provider (CASH, CARD, MPESA)
      amount,
      status: paymentStatus,
      userId,
    });
    
    // Return created payment with STK Push details if available
    if (stkPushResult) {
      return {
        payment: paymentWithDetails,
        stkPush: stkPushResult,
      };
    }
    
    // Return created payment
    return paymentWithDetails; // Return payment
  } catch (error) {
    // Rollback transaction on error
    await transaction.rollback(); // Rollback transaction
    throw error; // Re-throw error
  }
};

/**
 * Process Paystack payment
 * Processes payment through Paystack (initialize and handle callback)
 * @param {Object} paymentData - Payment data (sale_id, email, amount, metadata)
 * @param {number} userId - User ID processing the payment
 * @returns {Promise<Object>} Payment initialization response
 */
const processPaystackPayment = async (paymentData, userId) => {
  // Extract payment data
  const { sale_id, email, amount, metadata = {} } = paymentData; // Extract payment data
  
  // Validate required fields
  if (!sale_id || !email || !amount) {
    throw new ValidationError('Sale ID, email, and amount are required'); // Throw error if missing required fields
  }
  
  // Validate email format
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // Email regex pattern
  if (!emailRegex.test(email)) {
    throw new ValidationError('Invalid email format'); // Throw error if invalid email
  }
  
  // Validate amount
  if (amount <= 0) {
    throw new ValidationError('Payment amount must be greater than 0'); // Throw error if invalid amount
  }
  
  // Get sale
  const sale = await Sale.findByPk(sale_id); // Find sale by ID
  if (!sale) {
    throw new NotFoundError('Sale not found'); // Throw error if sale doesn't exist
  }
  
  // Check if sale can accept payments (not cancelled)
  if (sale.status === 'CANCELLED') {
    throw new ValidationError('Cannot process payment for cancelled sale'); // Throw error if sale cancelled
  }
  
  // Calculate remaining balance
  const remainingBalance = await calculateRemainingBalance(sale_id); // Get remaining balance
  
  // Validate payment amount doesn't exceed remaining balance
  if (amount > remainingBalance) {
    throw new ValidationError(`Payment amount (${amount}) exceeds remaining balance (${remainingBalance})`); // Throw error if amount exceeds balance
  }
  
  // Determine tender_number (1 or 2) - check existing payments for this sale
  const existingPayments = await Payment.findAll({
    where: {
      sale_id: sale_id, // Match sale ID
    },
  });
  
  // Calculate next tender number (max existing tender_number + 1, max 2)
  const maxTenderNumber = existingPayments.length > 0
    ? Math.max(...existingPayments.map(p => p.tender_number || 1))
    : 0;
  const tenderNumber = Math.min(maxTenderNumber + 1, 2); // Max 2 tenders
  
  // Validate we haven't exceeded 2 tenders
  if (existingPayments.length >= 2) {
    throw new ValidationError('Maximum of 2 payments per sale is allowed'); // Throw error if too many payments
  }
  
  // Generate payment reference
  const reference = generatePaymentReference(); // Generate unique reference
  
  // Initialize Paystack payment
  const paystackResponse = await initializePaystackPayment(
    amount, // Payment amount
    email, // Customer email
    reference, // Payment reference
    {
      ...metadata, // Additional metadata
      sale_id, // Sale ID in metadata
      user_id: userId, // User ID in metadata
    }
  );
  
  // Create pending payment record with tender_number
  const payment = await Payment.create({
    sale_id, // Link to sale
    provider: 'CARD', // Payment provider (updated enum)
    reference, // Payment reference
    amount, // Payment amount
    status: 'PENDING', // Payment status (pending until verified)
    paid_at: null, // Paid date (null until verified)
    tender_number: tenderNumber, // Tender sequence number (1 or 2)
  });
  
  // Log Paystack payment initialization
  logger.info(`Paystack payment initialized: ${reference}`, {
    paymentId: payment.id,
    saleId: sale_id,
    amount,
    email,
    userId,
    authorizationUrl: paystackResponse.data?.authorization_url, // Authorization URL
  });
  
  // Return payment and Paystack response
  return {
    payment, // Payment record
    paystack: paystackResponse, // Paystack response (includes authorization_url)
  };
};

/**
 * Verify and complete Paystack payment
 * Verifies payment with Paystack and updates payment status
 * @param {string} reference - Payment reference
 * @returns {Promise<Object>} Verified payment
 */
const verifyAndCompletePaystackPayment = async (reference) => {
  // Start database transaction
  const transaction = await sequelize.transaction(); // Start transaction
  
  try {
    // Find payment by reference
    const payment = await Payment.findOne({
      where: {
        reference, // Match payment reference
        provider: 'CARD', // Match provider (updated enum)
      },
      include: [
        {
          model: Sale, // Include sale details
          as: 'sale', // Use sale alias
        },
      ],
      transaction, // Use transaction
    });
    
    // If payment not found, throw error
    if (!payment) {
      throw new NotFoundError('Payment not found'); // Throw error if payment doesn't exist
    }
    
    // If payment already verified, return payment
    if (payment.status === 'SUCCESS') {
      await transaction.rollback(); // Rollback transaction (no changes needed)
      return payment; // Return payment
    }
    
    // Verify payment with Paystack
    const paystackResponse = await verifyPaystackPaymentAPI(reference); // Verify with Paystack API
    
    // Check if payment was successful
    const isSuccess = paystackResponse.data?.status === 'success'; // Check Paystack status
    
    // Update payment status
    if (isSuccess) {
      payment.status = 'SUCCESS'; // Set status to SUCCESS
      payment.paid_at = new Date(); // Set paid date to now
      await payment.save({ transaction }); // Save payment in transaction
      
      // Update sale status if fully paid
      await updateSaleStatusIfFullyPaid(payment.sale_id, transaction); // Update sale status
      
      // Log successful payment
      logger.info(`Paystack payment verified successfully: ${reference}`, {
        paymentId: payment.id,
        saleId: payment.sale_id,
      });
    } else {
      // Payment failed
      payment.status = 'FAILED'; // Set status to FAILED
      await payment.save({ transaction }); // Save payment in transaction
      
      // Log failed payment
      logger.warn(`Paystack payment verification failed: ${reference}`, {
        paymentId: payment.id,
        saleId: payment.sale_id,
        paystackStatus: paystackResponse.data?.status, // Paystack status
      });
    }
    
    // Commit transaction
    await transaction.commit(); // Commit transaction
    
    // Reload payment with associations
    const updatedPayment = await Payment.findByPk(payment.id, {
      include: [
        {
          model: Sale, // Include sale details
          as: 'sale', // Use sale alias
          attributes: ['id', 'invoice_no', 'total', 'status'], // Select specific attributes
        },
      ],
    });
    
    // Return updated payment
    return updatedPayment; // Return payment
  } catch (error) {
    // Rollback transaction on error
    await transaction.rollback(); // Rollback transaction
    throw error; // Re-throw error
  }
};

/**
 * Process mobile money payment (mock/stub)
 * Processes mobile money payment (mock implementation for now)
 * @param {Object} paymentData - Payment data (sale_id, amount, phone_number)
 * @param {number} userId - User ID processing the payment
 * @returns {Promise<Object>} Created payment
 */
const processMobileMoneyPayment = async (paymentData, userId) => {
  // Extract payment data
  const { sale_id, amount, phone_number } = paymentData; // Extract payment data
  
  // Validate required fields
  if (!sale_id || !amount || !phone_number) {
    throw new ValidationError('Sale ID, amount, and phone number are required'); // Throw error if missing required fields
  }
  
  // Validate phone number (basic validation)
  const phoneRegex = /^\+?[1-9]\d{1,14}$/; // Phone number regex (E.164 format)
  if (!phoneRegex.test(phone_number)) {
    throw new ValidationError('Invalid phone number format'); // Throw error if invalid phone number
  }
  
  // Validate amount
  if (amount <= 0) {
    throw new ValidationError('Payment amount must be greater than 0'); // Throw error if invalid amount
  }
  
  // Get sale for invoice number/reference
  const sale = await Sale.findByPk(sale_id);
  if (!sale) {
    throw new NotFoundError('Sale not found');
  }

  // Start transaction for STK Push
  const transaction = await sequelize.transaction();
  
  try {
    // Determine tender number (check existing payments)
    const existingPayments = await Payment.findAll({
      where: { sale_id },
      transaction,
    });
    
    if (existingPayments.length >= 2) {
      throw new ValidationError('Maximum of 2 payments per sale is allowed');
    }
    
    const maxTenderNumber = existingPayments.length > 0
      ? Math.max(...existingPayments.map(p => p.tender_number || 1))
      : 0;
    const tenderNumber = Math.min(maxTenderNumber + 1, 2);

    // Create pending payment record first
    const payment = await Payment.create({
      sale_id,
      provider: 'MPESA', // Updated enum
      amount,
      status: 'PENDING',
      mpesa_phone_number: phone_number,
      tender_number: tenderNumber,
    }, { transaction });

    // Initiate STK Push
    const stkResult = await mpesaService.initiateSTKPush({
      phoneNumber: phone_number,
      amount,
      accountReference: sale.invoice_no || `SALE-${sale_id}`,
      transactionDesc: `Payment for sale ${sale.invoice_no || sale_id}`,
    });

    // Update payment with checkout request ID (store in reference)
    payment.reference = stkResult.checkoutRequestID;
    await payment.save({ transaction });

    await transaction.commit();

    logger.info('M-Pesa STK Push initiated', {
      paymentId: payment.id,
      saleId: sale_id,
      checkoutRequestID: stkResult.checkoutRequestID,
      phoneNumber: phone_number,
      amount,
    });

    // Return payment with STK Push details
    return {
      payment,
      stkPush: {
        checkoutRequestID: stkResult.checkoutRequestID,
        customerMessage: stkResult.customerMessage,
      },
    };
  } catch (error) {
    await transaction.rollback();
    throw error;
  }
};

/**
 * Get payment by ID
 * Retrieves a payment with details
 * @param {number} paymentId - Payment ID
 * @returns {Promise<Object>} Payment with associations
 */
const getPayment = async (paymentId) => {
  // Find payment by ID
  const payment = await Payment.findByPk(paymentId, {
    include: [
      {
        model: Sale, // Include sale details
        as: 'sale', // Use sale alias
        attributes: ['id', 'invoice_no', 'total', 'status'], // Select specific attributes
      },
    ],
  });
  
  // If payment not found, throw error
  if (!payment) {
    throw new NotFoundError('Payment not found'); // Throw error if payment doesn't exist
  }
  
  // Return payment
  return payment; // Return payment
};

/**
 * List payments with filters and pagination
 * Retrieves paginated list of payments
 * @param {Object} options - Query options
 * @param {number} options.page - Page number (default: 1)
 * @param {number} options.limit - Items per page (default: 10)
 * @param {number|null} options.saleId - Filter by sale ID
 * @param {string|null} options.provider - Filter by provider
 * @param {string|null} options.status - Filter by status
 * @param {string|null} options.startDate - Filter by start date (ISO string)
 * @param {string|null} options.endDate - Filter by end date (ISO string)
 * @returns {Promise<Object>} Paginated payments list
 */
const listPayments = async (options = {}) => {
  // Extract options with defaults
  const {
    page = 1,
    limit = 10,
    saleId = null,
    provider = null,
    status = null,
    startDate = null,
    endDate = null,
  } = options; // Extract and set defaults
  
  // Build where clause
  const where = {}; // Initialize where clause
  
  // Add sale ID filter if provided
  if (saleId) {
    where.sale_id = saleId; // Filter by sale ID
  }
  
  // Add provider filter if provided
  if (provider) {
    where.provider = provider; // Filter by provider
  }
  
  // Add status filter if provided
  if (status) {
    where.status = status; // Filter by status
  }
  
  // Add date range filter if provided
  if (startDate || endDate) {
    where.created_at = {}; // Initialize date filter
    if (startDate) {
      where.created_at[Op.gte] = new Date(startDate); // Greater than or equal to start date
    }
    if (endDate) {
      // Set end date to end of day
      const endDateTime = new Date(endDate); // Create date object
      endDateTime.setHours(23, 59, 59, 999); // Set to end of day
      where.created_at[Op.lte] = endDateTime; // Less than or equal to end date
    }
  }
  
  // Calculate offset for pagination
  const offset = (page - 1) * limit; // Calculate offset
  
  // Build query options
  const queryOptions = {
    where, // Apply where clause
    include: [
      {
        model: Sale, // Include sale details
        as: 'sale', // Use sale alias
        attributes: ['id', 'invoice_no', 'total', 'status'], // Select specific attributes
      },
    ],
    limit: parseInt(limit), // Set limit
    offset: parseInt(offset), // Set offset
    order: [['created_at', 'DESC']], // Order by creation date descending (newest first)
  };
  
  // Execute query to get payments and total count
  const { count, rows } = await Payment.findAndCountAll(queryOptions); // Get payments with count
  
  // Calculate total pages
  const totalPages = Math.ceil(count / limit); // Calculate total pages
  
  // Return paginated results
  return {
    payments: rows, // Payment records
    pagination: {
      page: parseInt(page), // Current page
      limit: parseInt(limit), // Items per page
      total: count, // Total items
      totalPages, // Total pages
    },
  };
};

/**
 * Get payments for a sale
 * Retrieves all payments for a specific sale
 * @param {number} saleId - Sale ID
 * @returns {Promise<Array>} Array of payments
 */
const getSalePayments = async (saleId) => {
  // Get sale
  const sale = await Sale.findByPk(saleId); // Find sale by ID
  if (!sale) {
    throw new NotFoundError('Sale not found'); // Throw error if sale doesn't exist
  }
  
  // Get all payments for this sale
  const payments = await Payment.findAll({
    where: {
      sale_id: saleId, // Match sale ID
    },
    order: [['created_at', 'DESC']], // Order by creation date descending
  });
  
  // Calculate payment summary
  const totalPaid = payments
    .filter(p => p.status === 'SUCCESS') // Only successful payments
    .reduce((sum, payment) => sum + parseFloat(payment.amount), 0); // Sum payment amounts
  
  const remainingBalance = parseFloat(sale.total) - totalPaid; // Calculate remaining balance
  
  // Return payments with summary
  return {
    payments, // Payment records
    summary: {
      saleTotal: parseFloat(sale.total), // Sale total
      totalPaid, // Total paid amount
      remainingBalance: Math.max(0, remainingBalance), // Remaining balance (minimum 0)
      isFullyPaid: remainingBalance <= 0, // Is fully paid
    },
  };
};

/**
 * Query M-Pesa STK Push status
 * Queries the status of an STK Push payment
 * @param {string} checkoutRequestID - Checkout Request ID from STK Push
 * @returns {Promise<Object>} Query result with payment status
 */
const querySTKPushStatus = async (checkoutRequestID) => {
  // Find payment by checkout request ID (stored in reference field)
  const payment = await Payment.findOne({
    where: {
      reference: checkoutRequestID,
      provider: 'MPESA', // Updated enum
    },
    include: [
      {
        model: Sale,
        as: 'sale',
        attributes: ['id', 'invoice_no', 'total', 'status'],
      },
    ],
  });

  if (!payment) {
    throw new NotFoundError('Payment not found for this checkout request');
  }

  // Query STK Push status from M-Pesa
  const queryResult = await mpesaService.querySTKPushStatus(checkoutRequestID);

  // If payment is complete, update payment status
  if (queryResult.isComplete && payment.status === 'PENDING') {
    const transaction = await sequelize.transaction();
    try {
      payment.status = 'SUCCESS';
      payment.paid_at = new Date();
      payment.mpesa_transaction_code = queryResult.mpesaReceiptNumber;
      await payment.save({ transaction });

      // Update sale status if fully paid
      await updateSaleStatusIfFullyPaid(payment.sale_id, transaction);

      await transaction.commit();

      logger.info('M-Pesa payment confirmed', {
        paymentId: payment.id,
        checkoutRequestID,
        mpesaReceiptNumber: queryResult.mpesaReceiptNumber,
      });
    } catch (error) {
      await transaction.rollback();
      throw error;
    }
  }

  // Reload payment
  const updatedPayment = await Payment.findByPk(payment.id, {
    include: [
      {
        model: Sale,
        as: 'sale',
        attributes: ['id', 'invoice_no', 'total', 'status'],
      },
    ],
  });

  return {
    payment: updatedPayment,
    queryResult,
  };
};

/**
 * Handle M-Pesa callback/webhook
 * Processes M-Pesa callback and updates payment status
 * @param {Object} callbackData - Callback data from M-Pesa
 * @returns {Promise<Object>} Updated payment
 */
const handleMPesaCallback = async (callbackData) => {
  // Parse callback data
  const callbackInfo = mpesaService.handleMPesaCallback(callbackData);

  // Find payment by checkout request ID
  const payment = await Payment.findOne({
    where: {
      reference: callbackInfo.checkoutRequestID,
      provider: 'MPESA', // Updated enum
    },
    include: [
      {
        model: Sale,
        as: 'sale',
      },
    ],
  });

  if (!payment) {
    logger.warn('M-Pesa callback received for unknown payment', {
      checkoutRequestID: callbackInfo.checkoutRequestID,
    });
    throw new NotFoundError('Payment not found for this callback');
  }

  // Start transaction
  const transaction = await sequelize.transaction();
  
  try {
    // Update payment based on callback result
    if (callbackInfo.isSuccess) {
      payment.status = 'SUCCESS';
      payment.paid_at = new Date();
      payment.mpesa_transaction_code = callbackInfo.mpesaTransactionCode;
      await payment.save({ transaction });

      // Update sale status if fully paid
      await updateSaleStatusIfFullyPaid(payment.sale_id, transaction);

      logger.info('M-Pesa payment confirmed via callback', {
        paymentId: payment.id,
        checkoutRequestID: callbackInfo.checkoutRequestID,
        mpesaReceiptNumber: callbackInfo.mpesaReceiptNumber,
      });
    } else if (callbackInfo.isPending) {
      // Keep as PENDING, might retry
      logger.info('M-Pesa payment still pending', {
        paymentId: payment.id,
        checkoutRequestID: callbackInfo.checkoutRequestID,
        resultCode: callbackInfo.resultCode,
        resultDesc: callbackInfo.resultDesc,
      });
    } else {
      // Payment failed
      payment.status = 'FAILED';
      await payment.save({ transaction });

      logger.warn('M-Pesa payment failed', {
        paymentId: payment.id,
        checkoutRequestID: callbackInfo.checkoutRequestID,
        resultCode: callbackInfo.resultCode,
        resultDesc: callbackInfo.resultDesc,
      });
    }

    await transaction.commit();

    // Reload payment
    const updatedPayment = await Payment.findByPk(payment.id, {
      include: [
        {
          model: Sale,
          as: 'sale',
          attributes: ['id', 'invoice_no', 'total', 'status'],
        },
      ],
    });

    return {
      payment: updatedPayment,
      callbackInfo,
    };
  } catch (error) {
    await transaction.rollback();
    throw error;
  }
};

/**
 * Manually confirm M-Pesa payment (manager/admin only)
 * Allows manager/admin to manually confirm M-Pesa payment with transaction code
 * @param {number} paymentId - Payment ID
 * @param {string} mpesaTransactionCode - M-Pesa transaction code (receipt number)
 * @param {number} managerId - Manager/Admin user ID confirming the payment
 * @returns {Promise<Object>} Updated payment
 */
const manuallyConfirmMPesaPayment = async (paymentId, mpesaTransactionCode, managerId) => {
  // Find payment
  const payment = await Payment.findOne({
    where: {
      id: paymentId,
      provider: 'MPESA', // Updated enum
      status: 'PENDING',
    },
    include: [
      {
        model: Sale,
        as: 'sale',
      },
    ],
  });

  if (!payment) {
    throw new NotFoundError('Pending M-Pesa payment not found');
  }

  // Validate manager/admin
  const manager = await User.findByPk(managerId);
  if (!manager) {
    throw new NotFoundError('Manager not found');
  }
  if (manager.role !== 'manager' && manager.role !== 'system_admin') {
    throw new ValidationError('Only managers and admins can manually confirm M-Pesa payments');
  }

  // Start transaction
  const transaction = await sequelize.transaction();

  try {
    // Update payment status
    payment.status = 'SUCCESS';
    payment.paid_at = new Date();
    payment.mpesa_transaction_code = mpesaTransactionCode;
    await payment.save({ transaction });

    // Update sale status if fully paid
    await updateSaleStatusIfFullyPaid(payment.sale_id, transaction);

    await transaction.commit();

    // Log manual confirmation
    logger.info('M-Pesa payment manually confirmed by manager', {
      paymentId: payment.id,
      saleId: payment.sale_id,
      mpesaTransactionCode,
      managerId,
    });

    // Reload payment
    const updatedPayment = await Payment.findByPk(payment.id, {
      include: [
        {
          model: Sale,
          as: 'sale',
          attributes: ['id', 'invoice_no', 'total', 'status'],
        },
      ],
    });

    return updatedPayment;
  } catch (error) {
    await transaction.rollback();
    throw error;
  }
};

// Export service functions
module.exports = {
  createPayment, // Create payment function
  processPaystackPayment, // Process Paystack payment function
  verifyPaystackPayment: verifyAndCompletePaystackPayment, // Verify Paystack payment function (aliased)
  processMobileMoneyPayment, // Process mobile money payment function (STK Push)
  querySTKPushStatus, // Query STK Push status function
  handleMPesaCallback, // Handle M-Pesa callback function
  manuallyConfirmMPesaPayment, // Manually confirm M-Pesa payment function
  getPayment, // Get payment function
  listPayments, // List payments function
  getSalePayments, // Get sale payments function
  calculateRemainingBalance, // Calculate remaining balance function
  isSaleFullyPaid, // Check if sale fully paid function
  updateSaleStatusIfFullyPaid, // Update sale status if fully paid function
};
