package org.jpos.tcpay;

import org.jpos.iso.ISOMsg;
import org.jpos.tcpay.acquirer.JposAcquirerTranslator;
import org.jpos.tcpay.acquirer.JposAcquirerTranslatorFactory;
import org.jpos.tcpay.connection.AcquirerChannel;
import org.jpos.tcpay.connection.AcquirerChannelFactory;
import org.jpos.tcpay.constant.RouterConstants;
import org.jpos.tcpay.db.entity.*;
import org.jpos.tcpay.model.IsoAcquirerTerminal;
import org.jpos.tcpay.service.ReversalService;
import org.jpos.tcpay.service.TransactionAuxUtils;
import org.jpos.tcpay.service.TransactionLifeCycle;
import org.jpos.util.AppLogger;
import org.jpos.util.LogEvent;
import org.jpos.util.Logger;
import org.jpos.util.SimpleLogSource;

import java.net.SocketTimeoutException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

public class TransactionManagerImpl implements TransactionManager {
    
    private final AppLogger appLogger = new AppLogger();
    private final TransactionAuxUtils transactionAuxUtils;
    private final TransactionLifeCycle lifecycleService;

    // Transaction context
    private PosTerminal posTerminal;
    private AcquirerTerminal acquirerTerminal;
    private Acquirer acquirer;
    private AcquirerConnection acquirerConnection;
    private JposAcquirerTranslator translator;
    private AcquirerChannel acquirerChannel;
    private IsoAcquirerTerminal isoAcquirerTerminal;
    private Map<String, Object> metaData;

    private PosTempTransaction tempTransaction;
    

    public TransactionManagerImpl(TransactionAuxUtils transactionAuxUtils,
                                  TransactionLifeCycle lifecycleService,
                                  ReversalService reversalService) {
        super();
        this.transactionAuxUtils = transactionAuxUtils;
        this.lifecycleService = lifecycleService;
    }

    /**
     *  Check the mandatory fields and mapping of the Acquirer Terminal
     *  Is the Terminal Busy
     *  Get Acquirer and Acquirer Connection, Jpos Translator
     * @param jposRequest
     * @return true if all validations pass, false otherwise
     */
    @Override
    public boolean checkAndValidateRequest(ISOMsg jposRequest) {
        LogEvent evt = new LogEvent ("checkAndValidateRequest");

        try {

            // Extract DE-41 (stid) from message
            String stid = jposRequest.getString(41);
            if (stid == null || stid.trim().isEmpty()) {
                evt.addMessage("Missing or empty DE-41 (stid) field");
                return false;
            }
            evt.addMessage("DE-41 (stid): " + stid);
            // Identify the POS Terminal using DE-41
            posTerminal = transactionAuxUtils.getPosTerminalByTerminalId(stid);


            if (Objects.isNull(posTerminal)) {
                evt.addMessage("No Matching PosTerminal for stid: " + stid);
                return false;
            }

            acquirerTerminal = Optional.ofNullable(posTerminal).map(PosTerminal::getAcquirerTerminals)
                    .map(x -> x.get(0))
                    .orElseGet(() -> null);
            // Get First acquirer terminal (in practice, you might want more sophisticated selection)
            if (Objects.isNull(acquirerTerminal)) {
                evt.addMessage("No AcquirerTerminals configured for POS terminal: " + stid);
                return false;
            }
            evt.addMessage("BankTid: " + acquirerTerminal.getTerminalId());
            evt.addMessage("BankMid: " + acquirerTerminal.getAcquirerMerchant().getMerchantId());

            // Get the bank terminal ID and merchant ID for busy check
            String bankTid = acquirerTerminal.getTerminalId();
            String bankMid = acquirerTerminal.getAcquirerMerchant().getMerchantId();
            
            // Check and handle stale transactions before processing new request
            // TODO: This needs to taken care later

            // Check if terminal is busy
            // TODO: Kasi 09-10-2023 - Temporarily disabling busy check for testing
            /*if (transactionAuxUtils.isTerminalBusy(bankTid, bankMid)) {
                logger.log("Terminal is busy - Bank TID: " + bankTid + ", Bank MID: " + bankMid);
                return false;
            }*/

            // Get Respective Acquirer instance
            acquirer = acquirerTerminal.getAcquirer();
            acquirerConnection = transactionAuxUtils.getAcquirerConnectionByAcquirer(acquirer);
            if (Objects.isNull(acquirerConnection)) {
                evt.addMessage("No Matching AcquirerConnection for acquirer: " + acquirer.getName());
                return false;
            }
            evt.addMessage("Acquirer: " + acquirer.getName());

            // Get the translator
            translator = JposAcquirerTranslatorFactory.getJposAcquirerTranslator(acquirer.getName());
            if (Objects.isNull(translator)) {
                evt.addMessage("No Matching JposAcquirerTranslator for acquirer: " + acquirer.getName());
                return false;
            }

            // Get the acquirer channel
            acquirerChannel = AcquirerChannelFactory.getAcquirerChannel(acquirer.getName());
            if (Objects.isNull(acquirerChannel)) {
                evt.addMessage("No Matching AcquirerChannel for acquirer: " + acquirer.getName());
                return false;
            }

            evt.addMessage("Request validation successful for terminal: " + stid);
            return true;
            
        } catch (Exception e) {
            evt.addMessage("Exception during request validation: " + e.getMessage());
            evt.addMessage(e);
            return false;
        } finally {
            appLogger.log(evt);
        }
    }

    /**
     * Get the translator
     * Get the acquirer channel
     * Create IsoAcquirerTerminal
     * Manipulate meta data if needed
     * Trickle feed the offline txns if Required
     * @param jposRequest
     * @return true if onboarding is successful, false otherwise
     */
    @Override
    public boolean onboardRequest(ISOMsg jposRequest) {
        LogEvent evt = new LogEvent ("onboardRequest");

        try {
            // Create IsoAcquirerTerminal
            isoAcquirerTerminal = new IsoAcquirerTerminal(
                acquirerTerminal.getTerminalId(),
                acquirerTerminal.getAcquirerMerchant().getMerchantId(), 
                acquirer.getNii(),
                acquirer.getAcquirerInstitutionCode()
            );

            // Manipulate meta data
            metaData = manipulateMetaData(posTerminal);
            metaData.put(RouterConstants.ACQ_TERMINAL, acquirerTerminal);

            // TODO: Trickle feed the reversal txns if required
            
            // Start the transaction lifecycle - get temp transaction
            tempTransaction = lifecycleService.startTransaction(
                jposRequest, posTerminal, acquirerTerminal
            );
            
            // Store temp transaction in metadata for later use
            metaData.put("TEMP_TRANSACTION", tempTransaction);

            evt.addMessage("Request onboarded successfully. Temp transaction ID: " + tempTransaction.getId());
            return true;
            
        } catch (Exception e) {
            evt.addMessage("Exception during request onboarding: " + e.getMessage());
            evt.addMessage(e);
            return false;
        } finally {
            appLogger.log(evt);
        }
    }

    /**
     * Send and receive the Payload
     * Transform the response to POS format
     * @param jposRequest
     * @return the transformed response ISO message
     */
    @Override
    public ISOMsg processTransaction(ISOMsg jposRequest) {
        LogEvent evt = new LogEvent ("processTransaction");

        try {
            // Get temp transaction from metadata
            if (tempTransaction == null) {
                evt.addMessage("No temp transaction found in metadata");
                return generateErrorMsg(jposRequest);
            }
            
            // Translate the request
            ISOMsg acqRequest = translator.toMessage(jposRequest, isoAcquirerTerminal, metaData);
            if (Objects.isNull(acqRequest)) {
                evt.addMessage("No Request Generated For Jpos");
                
                // Move temp to failed before returning error
                lifecycleService.cleanupTransaction(tempTransaction, "Request translation failed");
                return generateErrorMsg(jposRequest);
            }

            // Send and receive the Payload with timeout monitoring
            ISOMsg acqResponse = null;
            try {
                // Start monitoring for response timeout
                long bankRequestStart = System.currentTimeMillis();
                
                acqResponse = acquirerChannel.transceive(acqRequest, acquirerConnection);
                
                long bankRequestDuration = System.currentTimeMillis() - bankRequestStart;
                if (Objects.nonNull(acqResponse)) {
                    evt.addMessage("Bank response received in " + bankRequestDuration + "ms for terminal: " +
                            acquirerTerminal.getTerminalId());
                }
            } catch (SocketTimeoutException communicationException) {
                appLogger.log("Socket Timedout  with acquirer: " + communicationException.getMessage());
                
                // Handle connection loss scenario
                lifecycleService.handleResponseTimeout(tempTransaction);
                
                // Return appropriate error
                return generateErrorMsg(jposRequest);
            }
            
            if (Objects.isNull(acqResponse)) {
                evt.addMessage("No Response From Acquirer " + acquirer.getName());
                
                // Trigger automatic reversal for timeout
                lifecycleService.handleConnectionLoss(tempTransaction);
                
                // Return timeout error to POS
                return generateTimeoutErrorMsg(jposRequest);
            }

            // Transform the response to POS format
            ISOMsg response = translator.fromMessage(acqResponse, jposRequest, metaData);

            // Handle success/failure and complete transaction lifecycle
            try {
                boolean success = lifecycleService.completeTransaction(tempTransaction, response);
                
                String responseCode = response.getString(39);
                if (success) {
                    evt.addMessage("Transaction successful for terminal " + acquirerTerminal.getTerminalId() +
                              ", response code: " + responseCode);
                } else {
                    evt.addMessage("Transaction failed for terminal " + acquirerTerminal.getTerminalId() +
                              ", response code: " + responseCode);
                }
                
            } catch (Exception dbError) {
                evt.addMessage("Database error during transaction completion: " + dbError.getMessage());
                
                // Handle database error with reversal
                lifecycleService.handleDatabaseError(tempTransaction, dbError);
                
                // Return system error to POS
                ISOMsg errorResponse = (ISOMsg)jposRequest.clone();
                try {
                    errorResponse.setMTI(jposRequest.getMTI());
                    errorResponse.setResponseMTI();
                } catch (Exception e) {
                    // Ignore MTI setting errors
                }
                errorResponse.set(39, "96"); // System error
                return errorResponse;
            }

            // return the response
            return response;
            
        } catch (Exception e) {
            evt.addMessage("Exception while processing the transaction: " + e.getMessage());
            evt.addMessage(e);
            
            // If we have a temp transaction that hasn't been moved yet, handle it properly
            if (tempTransaction != null && tempTransaction.getId() != null) {
                try {
                    // Use cleanup method to handle the error scenario
                    lifecycleService.cleanupTransaction(tempTransaction,
                        "Processing exception: " + e.getMessage());
                    
                } catch (Exception dbError) {
                    appLogger.log("Database error during exception handling: " + dbError.getMessage());
                    // Handle complex error scenario
                    lifecycleService.handleDatabaseError(tempTransaction, dbError);
                }
            }
            
            return generateErrorMsg(jposRequest);
        } finally {
            appLogger.log(evt);
        }
    }

    @Override
    public void cleanupTransaction() {
        try {
            if (tempTransaction != null) {
                lifecycleService.cleanupTransaction(tempTransaction, "Manual cleanup invoked");
                appLogger.log("Manual cleanup of temp transaction ID: " + tempTransaction.getId());
            }
        } catch (Exception e) {
            appLogger.log("Error during manual cleanup: " + e.getMessage());
        }
    }
    /**
     * Generate standard error response message
     */
    public ISOMsg generateErrorMsg(ISOMsg request) {
        try {
            ISOMsg errorResponse = (ISOMsg)request.clone();
            errorResponse.setMTI(request.getMTI());
            errorResponse.setResponseMTI();
            errorResponse.set(39, "06"); // Error
            return errorResponse;
        } catch (Exception e) {
            appLogger.log("Error creating error response: " + e.getMessage());
            return null;
        }
    }
    
    /**
     * Generate timeout error message for communication timeouts
     */
    private ISOMsg generateTimeoutErrorMsg(ISOMsg request) {
        try {
            ISOMsg timeoutResponse = (ISOMsg)request.clone();
            timeoutResponse.setMTI(request.getMTI());
            timeoutResponse.setResponseMTI();
            timeoutResponse.set(39, "68"); // Host timeout
            return timeoutResponse;
        } catch (Exception e) {
            appLogger.log("Error creating timeout response: " + e.getMessage());
            return generateErrorMsg(request);
        }
    }
    
    /**
     * Manipulate metadata for transaction processing
     */
    private Map<String, Object> manipulateMetaData(PosTerminal posTerminal) {
        Map<String, Object> metaData = new HashMap<>();
        Optional.of(posTerminal).map(PosTerminal::getSerialNumber).ifPresent(x  -> metaData.put(RouterConstants.SERIAL_NUMBER, x));
        // Add any additional metadata needed for processing
        metaData.put("PROCESSING_TIMESTAMP", System.currentTimeMillis());
        
        return metaData;
    }
}
