/*
 * 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.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.util.NotFoundException;
import org.jivesoftware.util.StringUtils;
import org.jivesoftware.xmpp.workgroup.AgentSession;
import org.jivesoftware.xmpp.workgroup.RequestQueue;
import org.jivesoftware.xmpp.workgroup.UserCommunicationMethod;
import org.jivesoftware.xmpp.workgroup.Workgroup;
import org.jivesoftware.xmpp.workgroup.chatbot.ChatbotSession;
import org.jivesoftware.xmpp.workgroup.spi.WorkgroupCompatibleClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.forms.DataForm;
import org.xmpp.forms.FormField;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;

/**
 * Requests made by users to get support by some agent.
 *
 * @author Gaston Dombiak
 */
public class UserRequest extends Request {

    private static final Logger Log = LoggerFactory.getLogger(UserRequest.class);
    
    private static final String INSERT_SESSION =
            "INSERT INTO fpSession(sessionID, userID, workgroupID, state, queueWaitTime, " +
            "startTime, endTime) values(?,?,?,?,?,?,?)";
    private static final String UPDATE_SESSION =
            "UPDATE fpSession SET state=?, queueWaitTime=?, endTime=? WHERE sessionID=?";
    private static final String INSERT_META_DATA =
            "INSERT INTO fpSessionMetadata(sessionID, metadataName, metadataValue) VALUES(?,?,?)";

    /**
     * Flag that indicates if an invitation that was not answered was offered again to the user.
     * The way in which the offer will be offered again to the user may vary according to the type
     * of client used by the user. For instance, if the user is using a chatbot to join the
     * workgroup then a new message may be sent to the user asking if he wants to receive a new
     * offer or cancel the request.
     */
    private boolean invitationChecked = false;

    private JID userJID;
    private String userID;
    private boolean anonymousUser;

    /**
     * Keeps the object that represents the type of client that the user is using.
     */
    private UserCommunicationMethod communicationMethod;
    private RequestQueue queue = null;
    private boolean savedToDB = false;
    /**
     * Timestamp that indicates when an invitation was sent to the user that made this request.
     */
    private long invitationSent;
    /**
     * ID of the room where the user was invited.
     */
    private String invitedRoomID;
    /**
     * Requests that are related to a user request. For instance, an invitation request sent to an
     * agent that is related to this user request.
     */
    private Queue<Request> relatedRequests = new ConcurrentLinkedQueue<Request>();

    /**
     * Returns an existing request given the requesting user's address and workgroup or throws
     * NotFoundException if none was found.
     *
     * @param workgroup the workgroup that the user us trying to join.
     * @param address the address to check.
     * @return a request given the requesting user's address and workgroup.
     * @throws org.jivesoftware.util.NotFoundException if the request could not be found.
     */
    public static UserRequest getRequest(Workgroup workgroup, JID address) throws NotFoundException {
        UserRequest request = null;
        for (RequestQueue requestQueue : workgroup.getRequestQueues()) {
            if (request == null) {
                request = requestQueue.getRequest(address);
            }
        }
        if (request == null) {
            Log.debug("Request not found for " +
                    address.toString());
            throw new NotFoundException();
        }
        return request;
    }

    public UserRequest(IQ packet, Workgroup wg) {
        super();
        this.userJID = packet.getFrom();
        this.userID = userJID.toBareJID();
        this.anonymousUser = false;
        this.workgroup = wg;
        // Requests to join a workgroup made using an IQ are assumed to be using a Workgroup
        // compatible client
        this.communicationMethod = WorkgroupCompatibleClient.getInstance();

        Iterator<Element> elementIter = packet.getChildElement().elementIterator();
        while (elementIter.hasNext()) {
            Element element = elementIter.next();
            if ("queue-notifications".equals(element.getName())) {
                setNotify(true);
            }
            else if ("user".equals(element.getName())) {
                userID = element.attributeValue("id");
                if (userID != null && userID.length() > 0) {
                    anonymousUser = !userJID.toString().equals(userID) &&
                            !userJID.toBareJID().equals(userID);
                }
            }
            else if ("metadata".equals(element.getName())) {
                for (Iterator<Element> i = element.elementIterator(); i.hasNext();) {
                    Element item = i.next();
                    if ("value".equals(item.getName())) {
                        String name = item.attributeValue("name");
                        if (name != null) {
                            metaData.put(name, Arrays.asList(item.getTextTrim()));
                        }
                    }
                }
            }
        }

        // Create metadata from submitted form.
        DataForm submittedForm = (DataForm)packet.getExtension(DataForm.ELEMENT_NAME,
                DataForm.NAMESPACE);

        for (FormField field : submittedForm.getFields()) {
            metaData.put(field.getVariable(), field.getValues());
        }

        // Omit certain items
        metaData.remove("password");
    }

    /**
     * Creates a new request made by a user using a chatbot as the communication media.
     *
     * @param session the chatbot session that holds all the information sent by the user.
     * @param wg      the workgroup that the user wants to join.
     */
    public UserRequest(ChatbotSession session, Workgroup wg) {
        super();
        this.userJID = session.getUserJID();
        this.userID = userJID.toBareJID();
        this.anonymousUser = false;
        this.workgroup = wg;
        // Use the chatbot of the session as the method to communicate with the user that
        // made the request
        this.communicationMethod = session.getChatbot();
        // Always set that users using a bot want to be notified of the position in the queue
        setNotify(true);
        // Use the stored attributes in the session as the metadata of the request
        metaData.putAll(session.getAttributes());

        // Make sure that the following keys are present in the metadata
        if (!metaData.containsKey("userID")) {
            metaData.put("userID", Arrays.asList(userJID.toString()));
        }
    }

    /**
     * Update the current position of the user in the queue. This will send
     * the packet directly to the user.
     *
     * @param isPolling true if using polling mode.
     */
    public void updateQueueStatus(boolean isPolling) {
        try {
            // Notify the user his status in the queue
            communicationMethod.notifyQueueStatus(workgroup.getJID(), userJID, this, isPolling);
        }
        catch (Exception e) {
            Log.error(e.getMessage(), e);
        }
    }

    @Override
    public void checkRequest(String roomID) {
        if (getInvitationSentTime() != null && !hasJoinedRoom()) {
            checkInvitation(roomID);
        }
        for (Request request : relatedRequests) {
            request.checkRequest(roomID);
        }
    }

    public void setRequestQueue(RequestQueue queue) {
        this.queue = queue;
    }

    public RequestQueue getRequestQueue() {
        return queue;
    }

    /**
     * Returns the ID of the room where the user was invited to have a chat with an agent or
     * <tt>null</tt> if the user was never invited.
     *
     * @return the ID of the room where the user was invited to have a chat with an agent or
     *         <tt>null</tt> if the user was never invited.
     */
    public String getInvitedRoomID() {
        return invitedRoomID;
    }

    /**
     * Returns the Date when an invitation to join a room was sent to the user that made this
     * request. Or answer <tt>null</tt> if an invitation was never sent.
     *
     * @return the Date when an invitation to join a room was sent to the user that made this
     *         request.
     */
    public Date getInvitationSentTime() {
        if (invitationSent > 0) {
            return new Date(invitationSent);
        }
        return null;
    }

    public int getPosition() {
        int pos = -1;
        if (queue != null) {
            pos = queue.getPosition(this);
        }
        return pos;
    }

    public int getTimeStatus() {
        int averageTime = queue == null ? 0 : queue.getAverageTime();
        int timeStatus;
        if (averageTime == 0) {
            timeStatus = (getPosition() + 1) * 15;
        }
        else {
            timeStatus = (getPosition() + 1) * averageTime;
        }
        return timeStatus;
    }

    @Override
    public JID getUserJID() {
        return userJID;
    }

    /**
     * Returns the user unique identification. If the user joined using an anonymous connection
     * then the userID will be the value of the ID attribute of the USER element. Otherwise, the
     * userID will be the bare JID of the user that made the request.
     *
     * @return the user unique identification.
     */
    public String getUserID() {
        return userID;
    }

    /**
     * Returns true if the request was made by a user that is using an anonymous session. For
     * anonymous user the userJID will be something like "server.com/a9d32h" and will vary
     * each time the user creates a new connection whilst the userID will represent the user
     * unique identification that may be used across several sessions.
     *
     * @return true if the request was made by a user that is using an anonymous session.
     */
    public boolean isAnonymousUser() {
        return anonymousUser;
    }

    /**
     * The request is being asked to verify if a new invitation must be sent to the user that
     * didn't answer the previous invitation. The request will only retry one more time.
     *
     * @param roomID the id of the room where the user was invited.
     */
    public void checkInvitation(String roomID) {
        if (!invitationChecked && !hasJoinedRoom() &&
                System.currentTimeMillis() - invitationSent > 10000) {
            invitationChecked = true;
            communicationMethod.checkInvitation(this);
        }
    }

    /**
     * Notification message saying that the request has been accepted by an agent and that
     * invitations have been sent to the agent and the user that made the request.
     *
     * @param roomID the id of the room for which invitations were sent.
     */
    public void invitationsSent(String roomID) {
        invitationSent = System.currentTimeMillis();
        invitedRoomID = roomID;
        communicationMethod.invitationsSent(this);
    }

    /**
     * Notification message saying that the user that made the request has joined the room to have
     * a chat with an agent.
     *
     * @param roomID the id of the room where the user has joined.
     */
    public void supportStarted(String roomID) {
        joinedRoom = System.currentTimeMillis();
        communicationMethod.supportStarted(this);
    }

    /**
     * Notification message saying that the support session has finished. At this point all room
     * occupants have left the room.
     */
    public void supportEnded() {
        communicationMethod.supportEnded(this);
    }

    @Override
    public void userJoinedRoom(JID roomJID, JID user) {
        // Notify related requests that new a occupant has joined the room
        for (Request request : relatedRequests) {
            request.userJoinedRoom(roomJID, user);
        }

    }

    /**
     * Adds a request that is somehow related to this user request. This is usually the case when
     * an invitation or transfer was sent to another agent.  For these cases a new Request is generated
     * that is related to this request. Since all these requests will be interested in the room activitiy
     * we need to propagate support events (i.e. supportStarted and supportEnded).
     *
     * @param request the request that is related to this request.
     */
    public void addRelatedRequest(Request request) {
        relatedRequests.add(request);
    }

    /**
     * Remvoes a request that is no longer related to this user request.
     *
     * @param request the request that is no longer related to this request.
     */
    public void removeRelatedRequest(Request request) {
        relatedRequests.remove(request);
    }

    @Override
    public void cancel(Request.CancelType type) {
        super.cancel(type);

        JID sender = workgroup.getJID();
        if (queue != null) {
            sender = queue.getWorkgroup().getJID();
            queue.removeRequest(this);
        }
        try {
            // Notify the user that he has left the queue
            communicationMethod.notifyQueueDepartued(sender, userJID, this, type);
        }
        catch (Exception e) {
            Log.error(e.getMessage(), e);
        }
    }

    @Override
    void addOfferContent(Element offerElement) {
        // Flag the offer as a user request
        offerElement.addElement("user-request");
        // Add custom extension that includes the userID if the session belongs to an
        // anonymous user
        if (isAnonymousUser()) {
            Element element = offerElement.addElement("user", "http://jivesoftware.com/protocol/workgroup");
            element.addAttribute("id", getUserID());
        }
    }

    @Override
    void addRevokeContent(Element revoke) {
        // Add custom extension that includes the userID if the session belongs to an
        // anonymous user
        if (isAnonymousUser()) {
            Element element = revoke.addElement("user", "http://jivesoftware.com/protocol/workgroup");
            element.addAttribute("id", getUserID());
        }
    }

    @Override
    public Element getSessionElement() {
        QName qName = DocumentHelper.createQName("session", DocumentHelper.createNamespace("", "http://jivesoftware.com/protocol/workgroup"));
        Element sessionElement = DocumentHelper.createElement(qName);
        sessionElement.addAttribute("id", requestID);
        sessionElement.addAttribute("workgroup", getWorkgroup().getJID().toString());
        return sessionElement;
    }
    /**
     * Sends an invitation to the agent that previously accepted the offer to join a room.
     * Agents need to join a room to be able to chat (and fulfil the request) with the
     * user that sent the request.
     *
     * @param agentSession the agent that previously accepted the offer.
     */
    @Override
    public void offerAccepted(AgentSession agentSession) {
        super.offerAccepted(agentSession);
        // Ask the workgroup to send invitations to the agent and to the user that made the
        // request. The Workgroup will create a MUC room and send invitations to the agent and
        // the user.
        getWorkgroup().sendInvitation(agentSession, this);
    }

    @Override
    public void updateSession(int state, long offerTime) {
        boolean inserted = false;
        long queueWaitTime = new Date().getTime() - offerTime;
        String tempDate = StringUtils.dateToMillis(new Date());

        // Gather all information needed.
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = DbConnectionManager.getConnection();
            synchronized (this) {
                if (!savedToDB) {
                    pstmt = con.prepareStatement(INSERT_SESSION);
                    pstmt.setString(1, requestID);
                    pstmt.setString(2, getUserID());
                    pstmt.setLong(3, getWorkgroup().getID());
                    pstmt.setInt(4, state);
                    pstmt.setLong(5, queueWaitTime);
                    pstmt.setString(6, tempDate);
                    pstmt.setString(7, tempDate);
                    pstmt.executeUpdate();
                    savedToDB = true;
                    inserted = true;
                }
                else {
                    pstmt = con.prepareStatement(UPDATE_SESSION);
                    pstmt.setInt(1, state);
                    pstmt.setLong(2, queueWaitTime);
                    pstmt.setString(3, tempDate);
                    pstmt.setString(4, requestID);
                    pstmt.executeUpdate();
                }
            }
        }
        catch (Exception ex) {
            Log.error(
                    "There was an issue handling offer update using sessionID " + requestID, ex);
        }
        finally {
           DbConnectionManager.closeConnection(pstmt, con);
        }
        if (inserted) {
            saveMetadata();
        }
    }

    private void saveMetadata() {
        final Map<String, List<String>> map = getMetaData();

        Connection con = null;
        try {
            con = DbConnectionManager.getConnection();
            PreparedStatement pstmt = con.prepareStatement(INSERT_META_DATA);
            for (String key : map.keySet()) {
                List<String> values = map.get(key);

                pstmt.setString(1, requestID);
                pstmt.setString(2, key);
                pstmt.setString(3, encodeMetadataValue(values));
                pstmt.executeUpdate();
            }
            pstmt.close();
        }
        catch (SQLException e) {
            Log.error(e.getMessage(), e);
        }
        finally {
            DbConnectionManager.closeConnection(con);
        }
    }
}
