/**
 * M-Pesa Payment Service
 * Integration with Safaricom Daraja API (STK Push + C2B)
 */

const axios = require('axios');
const crypto = require('crypto');
const { ValidationError } = require('../../../utils/errors');
const logger = require('../../../utils/logger');
const appConfig = require('../../../config/app');

// M-Pesa Daraja API configuration
const MPESA_CONFIG = {
  baseUrl: process.env.MPESA_BASE_URL || 'https://sandbox.safaricom.co.ke',
  consumerKey: process.env.MPESA_CONSUMER_KEY || '',
  consumerSecret: process.env.MPESA_CONSUMER_SECRET || '',
  passkey: process.env.MPESA_PASSKEY || '',
  shortcode: process.env.MPESA_SHORTCODE || '',
  timeoutUrl: process.env.MPESA_TIMEOUT_URL || `${process.env.API_BASE_URL || 'http://localhost:4000'}/api/payments/mpesa/callback`,
  callbackUrl: process.env.MPESA_CALLBACK_URL || `${process.env.API_BASE_URL || 'http://localhost:4000'}/api/payments/mpesa/callback`,
  accountReference: process.env.MPESA_ACCOUNT_REFERENCE || 'XYZ_POS',
  transactionDesc: process.env.MPESA_TRANSACTION_DESC || 'Payment for goods',
  timeoutSeconds: parseInt(process.env.MPESA_TIMEOUT_SECONDS) || 60, // Default 60 seconds
};

// Cache for access tokens (expires after 1 hour)
let accessTokenCache = {
  token: null,
  expiresAt: null,
};

/**
 * Get OAuth access token from Daraja API
 * @returns {Promise<string>} Access token
 */
const getAccessToken = async () => {
  // Check if cached token is still valid
  if (accessTokenCache.token && accessTokenCache.expiresAt > Date.now()) {
    return accessTokenCache.token;
  }

  if (!MPESA_CONFIG.consumerKey || !MPESA_CONFIG.consumerSecret) {
    throw new ValidationError('M-Pesa credentials not configured');
  }

  try {
    const auth = Buffer.from(`${MPESA_CONFIG.consumerKey}:${MPESA_CONFIG.consumerSecret}`).toString('base64');
    
    const response = await axios.get(`${MPESA_CONFIG.baseUrl}/oauth/v1/generate?grant_type=client_credentials`, {
      headers: {
        Authorization: `Basic ${auth}`,
      },
    });

    const { access_token, expires_in } = response.data;
    
    // Cache the token (expires 5 minutes before actual expiration for safety)
    accessTokenCache.token = access_token;
    accessTokenCache.expiresAt = Date.now() + (expires_in - 300) * 1000;

    return access_token;
  } catch (error) {
    logger.error('M-Pesa access token error:', error.response?.data || error.message);
    throw new ValidationError(`Failed to get M-Pesa access token: ${error.response?.data?.errorMessage || error.message}`);
  }
};

/**
 * Generate password for STK Push (Base64 encoded)
 * @param {string} timestamp - Timestamp string
 * @returns {string} Password
 */
const generatePassword = (timestamp) => {
  const passwordString = `${MPESA_CONFIG.shortcode}${MPESA_CONFIG.passkey}${timestamp}`;
  return Buffer.from(passwordString).toString('base64');
};

/**
 * Generate timestamp in format: YYYYMMDDHHmmss
 * @returns {string} Timestamp
 */
const generateTimestamp = () => {
  const now = new Date();
  const year = now.getFullYear();
  const month = String(now.getMonth() + 1).padStart(2, '0');
  const day = String(now.getDate()).padStart(2, '0');
  const hours = String(now.getHours()).padStart(2, '0');
  const minutes = String(now.getMinutes()).padStart(2, '0');
  const seconds = String(now.getSeconds()).padStart(2, '0');
  return `${year}${month}${day}${hours}${minutes}${seconds}`;
};

/**
 * Format phone number to M-Pesa format (254XXXXXXXXX)
 * @param {string} phoneNumber - Phone number in any format
 * @returns {string} Formatted phone number
 */
const formatPhoneNumber = (phoneNumber) => {
  // Remove all non-digit characters
  let cleaned = phoneNumber.replace(/\D/g, '');
  
  // Handle Kenyan numbers
  if (cleaned.startsWith('0')) {
    // Convert 07XXXXXXXX to 2547XXXXXXXX
    cleaned = '254' + cleaned.substring(1);
  } else if (cleaned.startsWith('254')) {
    // Already in correct format
    cleaned = cleaned;
  } else if (cleaned.startsWith('+254')) {
    // Remove the +
    cleaned = cleaned.substring(1);
  } else {
    // Assume it's a local number without 0
    cleaned = '254' + cleaned;
  }
  
  // Validate length (should be 12 digits: 254XXXXXXXXX)
  if (cleaned.length !== 12) {
    throw new ValidationError('Invalid phone number format. Use format: 0712345678 or +254712345678');
  }
  
  return cleaned;
};

/**
 * Initiate STK Push payment
 * @param {Object} params - STK Push parameters
 * @param {string} params.phoneNumber - Customer phone number
 * @param {number} params.amount - Payment amount
 * @param {string} params.accountReference - Account reference (e.g., invoice number)
 * @param {string} params.transactionDesc - Transaction description
 * @param {string} params.callbackUrl - Callback URL (optional, uses default if not provided)
 * @returns {Promise<Object>} STK Push response with CheckoutRequestID
 */
const initiateSTKPush = async (params) => {
  const { phoneNumber, amount, accountReference, transactionDesc, callbackUrl } = params;

  if (!MPESA_CONFIG.shortcode || !MPESA_CONFIG.passkey) {
    throw new ValidationError('M-Pesa STK Push not configured');
  }

  try {
    const accessToken = await getAccessToken();
    const timestamp = generateTimestamp();
    const password = generatePassword(timestamp);
    const formattedPhone = formatPhoneNumber(phoneNumber);

    const stkPushData = {
      BusinessShortCode: MPESA_CONFIG.shortcode,
      Password: password,
      Timestamp: timestamp,
      TransactionType: 'CustomerPayBillOnline',
      Amount: Math.round(amount), // M-Pesa expects whole numbers (KES)
      PartyA: formattedPhone,
      PartyB: MPESA_CONFIG.shortcode,
      PhoneNumber: formattedPhone,
      CallBackURL: callbackUrl || MPESA_CONFIG.callbackUrl,
      AccountReference: accountReference || MPESA_CONFIG.accountReference,
      TransactionDesc: transactionDesc || MPESA_CONFIG.transactionDesc,
    };

    const response = await axios.post(
      `${MPESA_CONFIG.baseUrl}/mpesa/stkpush/v1/processrequest`,
      stkPushData,
      {
        headers: {
          Authorization: `Bearer ${accessToken}`,
          'Content-Type': 'application/json',
        },
      }
    );

    const { ResponseCode, ResponseDescription, CheckoutRequestID, CustomerMessage } = response.data;

    if (ResponseCode === '0') {
      logger.info('M-Pesa STK Push initiated successfully', {
        CheckoutRequestID,
        PhoneNumber: formattedPhone,
        Amount: amount,
      });

      return {
        success: true,
        checkoutRequestID: CheckoutRequestID,
        customerMessage: CustomerMessage,
        responseCode: ResponseCode,
        responseDescription: ResponseDescription,
      };
    } else {
      logger.error('M-Pesa STK Push failed', {
        ResponseCode,
        ResponseDescription,
        PhoneNumber: formattedPhone,
        Amount: amount,
      });

      throw new ValidationError(`M-Pesa STK Push failed: ${ResponseDescription}`);
    }
  } catch (error) {
    logger.error('M-Pesa STK Push error:', error.response?.data || error.message);
    throw new ValidationError(
      `M-Pesa STK Push error: ${error.response?.data?.errorMessage || error.response?.data?.ResponseDescription || error.message}`
    );
  }
};

/**
 * Query STK Push status
 * @param {string} checkoutRequestID - Checkout Request ID from STK Push
 * @returns {Promise<Object>} Query response with payment status
 */
const querySTKPushStatus = async (checkoutRequestID) => {
  if (!MPESA_CONFIG.shortcode || !MPESA_CONFIG.passkey) {
    throw new ValidationError('M-Pesa not configured');
  }

  try {
    const accessToken = await getAccessToken();
    const timestamp = generateTimestamp();
    const password = generatePassword(timestamp);

    const queryData = {
      BusinessShortCode: MPESA_CONFIG.shortcode,
      Password: password,
      Timestamp: timestamp,
      CheckoutRequestID: checkoutRequestID,
    };

    const response = await axios.post(
      `${MPESA_CONFIG.baseUrl}/mpesa/stkpushquery/v1/query`,
      queryData,
      {
        headers: {
          Authorization: `Bearer ${accessToken}`,
          'Content-Type': 'application/json',
        },
      }
    );

    const { ResponseCode, ResponseDescription, ResultCode, ResultDesc, ResultParameters } = response.data;

    logger.info('M-Pesa STK Push query result', {
      CheckoutRequestID: checkoutRequestID,
      ResponseCode,
      ResultCode,
      ResultDesc,
    });

    // Parse result parameters if available
    let mpesaReceiptNumber = null;
    let transactionDate = null;
    let phoneNumber = null;
    let amount = null;

    if (ResultParameters && ResultParameters.ResultParameter) {
      for (const param of ResultParameters.ResultParameter) {
        if (param.Key === 'MpesaReceiptNumber') {
          mpesaReceiptNumber = param.Value;
        } else if (param.Key === 'TransactionDate') {
          transactionDate = param.Value;
        } else if (param.Key === 'PhoneNumber') {
          phoneNumber = param.Value;
        } else if (param.Key === 'Amount') {
          amount = parseFloat(param.Value);
        }
      }
    }

    return {
      responseCode: ResponseCode,
      responseDescription: ResponseDescription,
      resultCode: ResultCode,
      resultDesc: ResultDesc,
      mpesaReceiptNumber,
      transactionDate,
      phoneNumber,
      amount,
      isComplete: ResultCode === '0',
      isPending: ResultCode === '1032' || ResultCode === '1037', // User didn't complete or timeout
    };
  } catch (error) {
    logger.error('M-Pesa STK Push query error:', error.response?.data || error.message);
    throw new ValidationError(
      `M-Pesa query error: ${error.response?.data?.errorMessage || error.message}`
    );
  }
};

/**
 * Generate C2B payment URL (for manual payment entry fallback)
 * @param {Object} params - C2B parameters
 * @param {string} params.phoneNumber - Customer phone number
 * @param {number} params.amount - Payment amount
 * @param {string} params.accountReference - Account reference
 * @returns {Object} C2B payment instructions
 */
const generateC2BPaymentUrl = (params) => {
  const { phoneNumber, amount, accountReference } = params;
  const formattedPhone = formatPhoneNumber(phoneNumber);

  // C2B instructions for manual payment
  return {
    method: 'C2B',
    instructions: `Pay Ksh ${amount} to ${MPESA_CONFIG.shortcode} via M-Pesa. Reference: ${accountReference}`,
    phoneNumber: formattedPhone,
    accountReference,
    amount,
  };
};

/**
 * Handle M-Pesa callback/webhook
 * @param {Object} callbackData - Callback data from M-Pesa
 * @returns {Object} Parsed callback data
 */
const handleMPesaCallback = (callbackData) => {
  try {
    const { Body } = callbackData;
    const stkCallback = Body.stkCallback;

    if (!stkCallback) {
      throw new ValidationError('Invalid callback format');
    }

    const {
      MerchantRequestID,
      CheckoutRequestID,
      ResultCode,
      ResultDesc,
    } = stkCallback;

    let mpesaReceiptNumber = null;
    let transactionDate = null;
    let phoneNumber = null;
    let amount = null;

    // Parse callback items
    if (stkCallback.CallbackMetadata && stkCallback.CallbackMetadata.Item) {
      for (const item of stkCallback.CallbackMetadata.Item) {
        if (item.Name === 'MpesaReceiptNumber') {
          mpesaReceiptNumber = item.Value;
        } else if (item.Name === 'TransactionDate') {
          transactionDate = item.Value;
        } else if (item.Name === 'PhoneNumber') {
          phoneNumber = item.Value;
        } else if (item.Name === 'Amount') {
          amount = parseFloat(item.Value);
        }
      }
    }

    const isSuccess = ResultCode === 0;
    const isPending = ResultCode === 1032 || ResultCode === 1037; // User didn't complete or timeout

    logger.info('M-Pesa callback received', {
      CheckoutRequestID,
      ResultCode,
      ResultDesc,
      isSuccess,
      isPending,
      mpesaReceiptNumber,
    });

    return {
      merchantRequestID: MerchantRequestID,
      checkoutRequestID: CheckoutRequestID,
      resultCode: ResultCode,
      resultDesc: ResultDesc,
      mpesaReceiptNumber,
      mpesaTransactionCode: mpesaReceiptNumber, // Same as receipt number
      transactionDate,
      phoneNumber,
      amount,
      isSuccess,
      isPending,
    };
  } catch (error) {
    logger.error('M-Pesa callback parsing error:', error);
    throw new ValidationError(`Failed to parse M-Pesa callback: ${error.message}`);
  }
};

module.exports = {
  initiateSTKPush,
  querySTKPushStatus,
  generateC2BPaymentUrl,
  handleMPesaCallback,
  formatPhoneNumber,
  MPESA_CONFIG,
};

