/**
 * Copyright (C) 2004-2006 Jive Software. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jivesoftware.xmpp.workgroup.request;

import java.util.Arrays;

import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
import org.jivesoftware.util.NotFoundException;
import org.jivesoftware.xmpp.workgroup.AgentNotFoundException;
import org.jivesoftware.xmpp.workgroup.AgentSession;
import org.jivesoftware.xmpp.workgroup.RequestQueue;
import org.jivesoftware.xmpp.workgroup.Workgroup;
import org.jivesoftware.xmpp.workgroup.WorkgroupManager;
import org.jivesoftware.xmpp.workgroup.interceptor.RoomInterceptorManager;
import org.jivesoftware.xmpp.workgroup.routing.RoutingManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.muc.Invitation;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;
import org.xmpp.packet.PacketError;

/**
 * Request sent by an agent to transfer a session support to another agent.
 *
 * @author Gaston Dombiak
 */
public class TransferRequest extends Request {
	
	private static final Logger Log = LoggerFactory.getLogger(TransferRequest.class);
	
    /**
     * Time limit to wait for the invitee to join the support room. The limit is verified once the agent
     * accepted the offer or a MUC invitation was sent to the user.
     */
    private static final long JOIN_TIMEOUT = 60 * 1000;

    private Type type;
    private String sessionID;
    private JID inviter;
    /**
     * JID of the entity that will receive the transfer offer. This is not necessarily the JID of the
     * user since when inviting a workgroup or a queue the JID will be the JID of the queue or workgroup.
     * Check out actualInvitee to learn the actual entity that got the transfer offer.
     */
    private JID invitee;
    /**
     * JID of the entity that ended up receiving the transfer offer.
     */
    private JID actualInvitee;
    private String reason;
    /**
     * Original request made by a user. The invitation being sent is related to this user request.
     */
    private UserRequest userRequest;
    /**
     * Timestamp when the offer was accepted by the agent or when the MUC invitation was sent to a user.
     * This information will be used to monitor if user actually joined the support room before the limit
     * timeout is reached.
     */
    private long offerAccepted;

    public TransferRequest(IQ packet, Workgroup workgroup) {
        super();
        Element iq = packet.getChildElement();
        this.type = Type.valueOf(iq.attributeValue("type"));
        Element sessionElement = iq.element("session");
        sessionID = sessionElement.attributeValue("id");
        inviter = packet.getFrom();
        invitee = new JID(iq.elementTextTrim("invitee"));
        reason = iq.elementTextTrim("reason");
        this.workgroup = workgroup;
        String jid = sessionElement.attributeValue("workgroup");
        try {
            if (jid == null) {
                // Keep backwards compatibility. This "trick" will only work while the workgroup
                // that received the user request and that is making the transfer is the
                // same workgroup.
                userRequest = workgroup.getUserRequest(sessionID);
            }
            else {
                JID workgroupJID = new JID(jid);
		// Replace the workgroup if the original offer originated from a different
		// workgroup
		this.workgroup = WorkgroupManager.getInstance().getWorkgroup(workgroupJID);
		userRequest = this.workgroup.getUserRequest(sessionID);
            }
            // Notify the user request that is now related to this new request
            userRequest.addRelatedRequest(this);
            // Add metadata of original user request to this offer
            if (userRequest.getMetaData() != null) {
                metaData.putAll(userRequest.getMetaData());
            }
        }
        catch (Exception e) {
            Log.error("Workgroup not found for transfer: " + jid, e);
        }
    }

    public void execute() {
        if (Type.user == type) {
            AgentSession agentSession = null;
            // Verify if the invitee user is an agent that is currently logged
            try {
                agentSession = WorkgroupManager.getInstance().getAgentManager().getAgent(invitee).getAgentSession();
            }
            catch (AgentNotFoundException e) {
                // Ignore
            }

            // Only send muc invites to a particular user.
            if (true) {
                // Invitee is not an agent so send a standard MUC room invitation
                sendMUCInvitiation();
                // Keep track when the invitation was sent to the user
                offerAccepted = System.currentTimeMillis();
            }
            else {
                // Invite the agent to the room by sending an offer
                Workgroup workgroup = agentSession.getWorkgroups().iterator().next();
                RequestQueue requestQueue = workgroup.getRequestQueues().iterator().next();
                // Add the requested agent as the initial target agent to get the offer
                getMetaData().put("agent", Arrays.asList(invitee.toString()));
                getMetaData().put("ignore", Arrays.asList(inviter.toBareJID()));
                // Dispatch the request
                requestQueue.getDispatcher().injectRequest(this);
            }
        }
        else if (Type.queue == type) {
            // Send offer to the best again available in the requested queue
            Workgroup targetWorkgroup = WorkgroupManager.getInstance().getWorkgroup(invitee.getNode());
            if (targetWorkgroup == null) {
                // No workgroup was found for the specified invitee. Send a Message with the error
                sendErrorMessage("Specified workgroup was not found.");
                return;
            }
            try {
                RequestQueue requestQueue = targetWorkgroup.getRequestQueue(invitee.getResource());
                getMetaData().put("ignore", Arrays.asList(inviter.toBareJID()));
                // Dispatch the request
                requestQueue.getDispatcher().injectRequest(this);
            }
            catch (NotFoundException e) {
                // No queue was found for the specified invitee. Send a Message with the error
                sendErrorMessage("Specified queue was not found.");
            }
        }
        else if (Type.workgroup == type) {
            // Select the best queue based on the original request
            Workgroup targetWorkgroup = WorkgroupManager.getInstance().getWorkgroup(invitee.getNode());
            if (targetWorkgroup != null) {
                RequestQueue requestQueue = RoutingManager.getInstance().getBestQueue(targetWorkgroup, userRequest);
                getMetaData().put("ignore", Arrays.asList(inviter.toBareJID()));
                // Send offer to the best again available in the requested queue
                requestQueue.getDispatcher().injectRequest(this);
            }
            else {
                // No workgroup was found for the specified invitee. Send a Message with the error
                sendErrorMessage("Specified workgroup was not found.");
            }
        }
    }


    @Override
	public void updateSession(int state, long offerTime) {
        // Ignore
    }

    @Override
	public void offerAccepted(AgentSession agentSession) {
        super.offerAccepted(agentSession);
        // Keep track when the offer was accepted by the agent
        offerAccepted = System.currentTimeMillis();
    }

    @Override
	public boolean sendOffer(AgentSession session, RequestQueue queue) {
        // Keep track of the actual entity that received the transfer offer
        actualInvitee = session.getJID();
        return super.sendOffer(session, queue);
    }

    @Override
	void addOfferContent(Element offerElement) {
        Element inviteElement = offerElement.addElement("transfer", "http://jabber.org/protocol/workgroup");

        inviteElement.addAttribute("type", type.toString());

        inviteElement.addElement("inviter").setText(inviter.toString());

        String serviceName = WorkgroupManager.getInstance().getMUCServiceName();
        inviteElement.addElement("room").setText(sessionID + "@" + serviceName);

        inviteElement.add(userRequest.getMetaDataElement());

        inviteElement.addElement("reason").setText(reason);
    }

    @Override
	void addRevokeContent(Element revoke) {
    }

    @Override
	public Element getSessionElement() {
        // Add the workgroup of the original user request
        QName qName = DocumentHelper.createQName("session", DocumentHelper.createNamespace("", "http://jivesoftware.com/protocol/workgroup"));
        Element sessionElement = DocumentHelper.createElement(qName);
        sessionElement.addAttribute("id", sessionID);
        sessionElement.addAttribute("workgroup", userRequest.getWorkgroup().getJID().toString());
        return sessionElement;
    }

    @Override
	JID getUserJID() {
        return userRequest.getUserJID();
    }

    @Override
	public void userJoinedRoom(JID roomJID, JID user) {
        Log.debug("User "+user+" has joined "+roomJID+". User should be kicked.");
        if (actualInvitee != null && actualInvitee.toBareJID().equals(user.toBareJID())) {
            joinedRoom = System.currentTimeMillis();
            // This request has been completed so remove it from the list of related
            // requests of the original user request
            userRequest.removeRelatedRequest(this);
            // Kick the inviter from the room
            IQ kick = new IQ(IQ.Type.set);
            kick.setTo(roomJID);
            kick.setFrom(workgroup.getFullJID());
            Element childElement = kick.setChildElement("query", "http://jabber.org/protocol/muc#admin");
            Element item = childElement.addElement("item");
            item.addAttribute("jid", inviter.toString());
            item.addAttribute("role", "none");
            item.addElement("reason").setText("Transfer was successful");
            workgroup.send(kick);
            Log.debug("Sent kicked to  "+user+" in room "+roomJID+".");
        }
    }

    @Override
	public void checkRequest(String roomID) {
        // Monitor that the agent/user joined the room and if not send back an error to the inviter
        if (offerAccepted > 0 && !hasJoinedRoom() && System.currentTimeMillis() - offerAccepted > JOIN_TIMEOUT) {
            Log.debug("Agent or user failed to join room "+roomID);
            // Send error message to inviter
            sendErrorMessage("Agent or user failed to join the room.");
        }
    }

    private void sendErrorMessage(String body) {
        // Invitation request has failed. Inform inviter
        userRequest.removeRelatedRequest(this);

        Message message = new Message();
        message.setError(PacketError.Condition.recipient_unavailable);
        message.setTo(inviter);
        message.setFrom(workgroup.getJID());
        message.setBody(body);
        Element element = message.addChildElement("transfer", "http://jabber.org/protocol/workgroup");
        element.addAttribute("type", type.toString());
        Element sessionElement = element.addElement("session", "http://jivesoftware.com/protocol/workgroup");
        sessionElement.addAttribute("id", sessionID);
        element.addElement("inviter").setText(inviter.toString());
        element.addElement("invitee").setText(invitee.toString());
        workgroup.send(message);
    }

    /**
     * Sends a standard MUC invitation to the invitee.
     */
    private void sendMUCInvitiation() {
        // Keep track of the actual entity that received the transfer offer
        actualInvitee = invitee;

        final String serviceName = WorkgroupManager.getInstance().getMUCServiceName();
        final String roomName = sessionID + "@" + serviceName;

        Invitation invitation = new Invitation(invitee.toString(), reason);
        invitation.setTo(roomName);
        invitation.setFrom(inviter);
        // Add workgroup extension that includes the JID of the workgroup
        Element element = invitation.addChildElement("workgroup", "http://jabber.org/protocol/workgroup");
        element.addAttribute("jid", workgroup.getJID().toBareJID());
        // Add custom extension that includes the sessionID
        element = invitation.addChildElement("session", "http://jivesoftware.com/protocol/workgroup");
        element.addAttribute("id", sessionID);
        RoomInterceptorManager interceptorManager = RoomInterceptorManager.getInstance();
        interceptorManager.invokeInterceptors(workgroup.getJID().toBareJID(), invitation, false, false);
        workgroup.send(invitation);
        interceptorManager.invokeInterceptors(workgroup.getJID().toBareJID(), invitation, false, true);
    }

    /**
     * Type of entity to receive the transfer of a groupchat support session.
     */
    public static enum Type {
        /**
         * A user is going to receive the transfer of a groupchat support session. The user could be another agent
         * or just a regular XMPP user.
         */
        user,
        /**
         * Some agent of the specified queue is going to receive the transfer of the groupchat support session.
         */
        queue,
        /**
         * Some agent of the specified workgroup is going to receive the transfer of the groupchat support session.
         */
        workgroup
    }
}
