package org.apache.sandesha2.workers;

import org.apache.axis2.AxisFault;
import org.apache.axis2.Constants;
import org.apache.axis2.addressing.AddressingConstants;
import org.apache.axis2.addressing.EndpointReference;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.engine.AxisEngine;
import org.apache.axis2.transport.RequestResponseTransport;
import org.apache.axis2.util.MessageContextBuilder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.sandesha2.RMMsgContext;
import org.apache.sandesha2.Sandesha2Constants;
import org.apache.sandesha2.SandeshaException;
import org.apache.sandesha2.storage.SandeshaStorageException;
import org.apache.sandesha2.storage.StorageManager;
import org.apache.sandesha2.storage.Transaction;
import org.apache.sandesha2.storage.beanmanagers.InvokerBeanMgr;
import org.apache.sandesha2.storage.beans.InvokerBean;
import org.apache.sandesha2.storage.beans.RMDBean;
import org.apache.sandesha2.util.MsgInitializer;
import org.apache.sandesha2.util.SandeshaUtil;
import org.apache.sandesha2.util.TerminateManager;
import org.apache.sandesha2.wsrm.Sequence;

public class InvokerWorker extends SandeshaWorker implements Runnable {

	ConfigurationContext configurationContext = null;
	String messageContextKey;
	boolean ignoreNextMsg = false;
	
	Log log = LogFactory.getLog(InvokerWorker.class);
	
	public InvokerWorker (ConfigurationContext configurationContext, String messageContextKey, boolean ignoreNextMsg) {
		this.configurationContext = configurationContext;
		this.messageContextKey = messageContextKey;
		this.ignoreNextMsg = ignoreNextMsg;
	}
	
	public void run() {
		if(log.isDebugEnabled()) log.debug("Enter: InvokerWorker::run");
		
		Transaction transaction = null;
		MessageContext msgToInvoke = null;
		
		try {
			
			StorageManager storageManager = SandeshaUtil.getSandeshaStorageManager(configurationContext,configurationContext.getAxisConfiguration());
			InvokerBeanMgr invokerBeanMgr = storageManager.getInvokerBeanMgr();
			
			//starting a transaction
			transaction = storageManager.getTransaction();
			
			InvokerBean invokerBean = invokerBeanMgr.retrieve(messageContextKey);

			msgToInvoke = storageManager.retrieveMessageContext(messageContextKey, configurationContext);

			// ending the transaction before invocation.
			if(transaction != null) {
				transaction.commit();
				transaction = storageManager.getTransaction();
			}
			
			RMMsgContext rmMsg = MsgInitializer.initializeMessage(msgToInvoke);

			// Lock the RMD Bean just to avoid deadlocks
			SandeshaUtil.getRMDBeanFromSequenceId(storageManager, invokerBean.getSequenceID());
			// Depending on the transaction  support, the service will be invoked only once. 
			// Therefore we delete the invoker bean and message now, ahead of time
			invokerBeanMgr.delete(messageContextKey);
			// removing the corresponding message context as well.
			storageManager.removeMessageContext(messageContextKey);

			try {

				boolean postFailureInvocation = false;

				// StorageManagers should st following property to
				// true, to indicate that the message received comes
				// after a failure.
				String postFaulureProperty = (String) msgToInvoke
						.getProperty(Sandesha2Constants.POST_FAILURE_MESSAGE);
				if (postFaulureProperty != null
						&& Sandesha2Constants.VALUE_TRUE.equals(postFaulureProperty))
					postFailureInvocation = true;

				if (postFailureInvocation) {
					makeMessageReadyForReinjection(msgToInvoke);
					if (log.isDebugEnabled())
						log.debug("Receiving message, key=" + messageContextKey + ", msgCtx="
								+ msgToInvoke.getEnvelope().getHeader());
					AxisEngine.receive(msgToInvoke);
				} else {
					if (log.isDebugEnabled())
						log.debug("Resuming message, key=" + messageContextKey + ", msgCtx="
								+ msgToInvoke.getEnvelope().getHeader());
					msgToInvoke.setPaused(false);
					AxisEngine.resumeReceive(msgToInvoke);
				}
				
				if (transaction != null && transaction.isActive()){
					transaction.commit();
				}
			} catch (Exception e) {
				if (log.isDebugEnabled())
					log.debug("Exception :", e);
				
			    if (transaction != null && transaction.isActive()){
			        transaction.rollback();			    	
			    }
			    handleFault(rmMsg, e);
			}


			transaction = storageManager.getTransaction();
			 
			if (rmMsg.getMessageType() == Sandesha2Constants.MessageTypes.APPLICATION) {
				Sequence sequence = (Sequence) rmMsg
						.getMessagePart(Sandesha2Constants.MessageParts.SEQUENCE);
				
				boolean highestMessage = false;
				if (sequence.getLastMessage() != null) {
					//this will work for RM 1.0 only
					highestMessage = true;
				} else {
					RMDBean rmdBean = SandeshaUtil.getRMDBeanFromSequenceId(storageManager, invokerBean.getSequenceID());
					
					if (rmdBean!=null && rmdBean.isTerminated()) {
						long highestInMsgNo = rmdBean.getHighestInMessageNumber();
						if (invokerBean.getMsgNo()==highestInMsgNo)
							highestMessage = true;
					}
				}
				
				if (highestMessage) {
					//do cleaning stuff that hs to be done after the invocation of the last message.
					TerminateManager.cleanReceivingSideAfterInvocation(invokerBean.getSequenceID(), storageManager);
					// exit from current iteration. (since an entry
					// was removed)
					if(log.isDebugEnabled()) log.debug("Exit: InvokerWorker::run Last message return");	
					if(transaction != null && transaction.isActive()) transaction.commit();
					return;
				}
			}
			
			if(!ignoreNextMsg){
				// updating the next msg to invoke
				RMDBean rMDBean = storageManager.getRMDBeanMgr().retrieve(invokerBean.getSequenceID());
				long nextMsgNo = rMDBean.getNextMsgNoToProcess();
				
				if (!(invokerBean.getMsgNo()==nextMsgNo)) {
					String message = "Operated message number is different from the Next Message Number to invoke";
					throw new SandeshaException (message);
				}
				
				nextMsgNo++;
				rMDBean.setNextMsgNoToProcess(nextMsgNo);
				storageManager.getRMDBeanMgr().update(rMDBean);
			}
			
			if(transaction != null && transaction.isActive()) transaction.commit();
			transaction = null;
			
		} catch (Exception e) {
			if (log.isErrorEnabled())
				log.error(e.toString(), e);
		} finally {
			if (workId !=null && lock!=null) {
				lock.removeWork(workId);
			}

			if (transaction!=null && transaction.isActive()) {
				try {
					transaction.rollback();
				} catch (SandeshaStorageException e) {
					if (log.isWarnEnabled())
						log.warn("Caught exception rolling back transaction", e);
				}
			}
		}
		
		if(log.isDebugEnabled()) log.debug("Exit: InvokerWorker::run");
	}

	private void makeMessageReadyForReinjection(MessageContext messageContext) {
		messageContext.setProperty(AddressingConstants.WS_ADDRESSING_VERSION, null);
		messageContext.getOptions().setMessageId(null);
		messageContext.getOptions().setTo(null);
		messageContext.getOptions().setAction(null);
		messageContext.setProperty(Sandesha2Constants.APPLICATION_PROCESSING_DONE, Sandesha2Constants.VALUE_TRUE);
	}

	private void handleFault(RMMsgContext inRMMsgContext, Exception e) {
		MessageContext inMsgContext = inRMMsgContext.getMessageContext();
		try {					
			MessageContext faultContext = MessageContextBuilder.createFaultMessageContext(inMsgContext, e);
			// Copy some of the parameters to the new message context.
			faultContext.setProperty(Constants.Configuration.CONTENT_TYPE, inMsgContext
					.getProperty(Constants.Configuration.CONTENT_TYPE));

			EndpointReference faultEPR = inRMMsgContext.getFaultTo();
			if (faultEPR==null)
				faultEPR = inRMMsgContext.getReplyTo();
			
			//we handler the WSRM Anon InOut scenario differently here
			if (Sandesha2Constants.SPEC_VERSIONS.v1_0.equals(inRMMsgContext.getRMSpecVersion())
					&& (faultEPR==null || faultEPR.hasAnonymousAddress())) {
				RequestResponseTransport requestResponseTransport = (RequestResponseTransport) inRMMsgContext.getProperty(RequestResponseTransport.TRANSPORT_CONTROL);
				
				//this will cause the fault to be thrown out of thread waiting on this transport object.
				AxisFault fault = new AxisFault ("Sandesha2 got a fault when doing the invocation", faultContext);
				if (requestResponseTransport!=null)
					requestResponseTransport.signalFaultReady(fault);
				else {
                    sendFaultIfPossible(faultContext);
                }
            } else	{
                sendFaultIfPossible(faultContext);
            }
        } catch (AxisFault e1) {
			if (log.isErrorEnabled())
				log.error("Unable to send fault message ", e1);
		}
	}

    /**
     * Trying to fix the error given by Axis2 when we try to send fault without required info.
     * This happens because duplicate handling is not correctly eliminating application response message.
     * This is a temporary  work around until the bug is fixed. 
     * @param faultContext
     * @throws AxisFault
     */
    private void sendFaultIfPossible(MessageContext faultContext) throws AxisFault {
        EndpointReference epr = null;
        String transportURL = (String) faultContext
                .getProperty(Constants.Configuration.TRANSPORT_URL);

        if (transportURL != null) {
            epr = new EndpointReference(transportURL);
        } else if (faultContext.getTo() != null
                && !faultContext.getTo().hasAnonymousAddress()) {
            epr = faultContext.getTo();
        }
        Object transportOut = faultContext.getProperty(MessageContext.TRANSPORT_OUT);

        if (epr == null && transportOut == null) {
            log.warn("Both the TO and MessageContext.TRANSPORT_OUT property are null," +
                    " so nowhere to send the fault");
        } else {
            AxisEngine.sendFault(faultContext);
        }
    }

}
