/**
 * $Revision: $
 * $Date: $
 *
 * Copyright (C) 2005-2008 Jive Software. All rights reserved.
 *
 * This software is published under the terms of the GNU Public License (GPL),
 * a copy of which is included in this distribution, or a commercial license
 * agreement with Jive.
 */

package org.jivesoftware.openfire.commands;

import org.dom4j.Element;
import org.dom4j.QName;
import org.jivesoftware.openfire.component.ComponentEventListener;
import org.jivesoftware.openfire.component.InternalComponentManager;
import org.jivesoftware.openfire.event.SessionEventDispatcher;
import org.jivesoftware.openfire.event.SessionEventListener;
import org.jivesoftware.openfire.session.Session;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.StringUtils;
import org.xmpp.forms.DataForm;
import org.xmpp.forms.FormField;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.PacketError;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * An AdHocCommandManager is responsible for keeping the list of available commands offered
 * by a service and for processing commands requests. Typically, instances of this class
 * are private to the service offering ad-hoc commands.
 *
 * @author Gaston Dombiak
 */
public class AdHocCommandManager implements SessionEventListener, ComponentEventListener {

    private static final String NAMESPACE = "http://jabber.org/protocol/commands";

    /**
     * Map that holds the offered commands by this service. Note: Key=commandCode, Value=command.
     * commandCode matches the node attribute sent by command requesters.
     */
    private Map<String, AdHocCommand> commands = new ConcurrentHashMap<String, AdHocCommand>();
    /**
     * Map that holds the id of command sessions of each requester.
     * Note: Key=requester full's JID, Value=collection with command sessions ids
     */
    private Map<String, Collection<String>> sessionsCounter = new ConcurrentHashMap<String, Collection<String>>();
    /**
     * Map that holds the command sessions. Used mainly to quickly locate a SessionData.
     * Note: Key=sessionID, Value=SessionData
     */
    private Map<String, SessionData> sessions = new ConcurrentHashMap<String, SessionData>();

    /**
     * Adds a new command to the list of supported ad-hoc commands by this server. The new
     * command will appear in the discoverable items list and will be executed for those users
     * with enough permission.
     *
     * @param command the new ad-hoc command to add.
     */
    public void addCommand(AdHocCommand command) {
        commands.put(command.getCode(), command);
    }

    /**
     * Removes the command from the list of ad-hoc commands supported by this server. The command
     * will no longer appear in the discoverable items list.
     *
     * @param command the ad-hoc command to remove.
     * @return true if the requested command was removed from the list of available commands.
     */
    public boolean removeCommand(AdHocCommand command) {
        return commands.remove(command.getCode()) != null;
    }

    /**
     * Returns a list with the available commands in this command manager.
     *
     * @return a list with the available commands in this command manager.
     */
    public Collection<AdHocCommand> getCommands() {
        return commands.values();
    }

    /**
     * Returns the command whose code matches the specified code or <tt>null</tt> if none
     * was found.
     *
     * @param code the code of the command to find.
     * @return the command whose code matches the specified code or null if none
     *         was found.
     */
    public AdHocCommand getCommand(String code) {
        return commands.get(code);
    }

    public IQ process(IQ packet) {
        IQ reply = IQ.createResultIQ(packet);
        Element iqCommand = packet.getChildElement();

        // Only packets of type SET can be processed
        if (!IQ.Type.set.equals(packet.getType())) {
            // Answer a bad_request error
            reply.setChildElement(iqCommand.createCopy());
            reply.setError(PacketError.Condition.bad_request);
            return reply;
        }

        String sessionid = iqCommand.attributeValue("sessionid");
        String commandCode = iqCommand.attributeValue("node");
        String from = packet.getFrom().toString();
        AdHocCommand command = commands.get(commandCode);
        if (sessionid == null) {
            // A new execution request has been received. Check that the command exists
            if (command == null) {
                // Requested command does not exist so return item_not_found error.
                reply.setChildElement(iqCommand.createCopy());
                reply.setError(PacketError.Condition.item_not_found);
            }
            else {
                // Check that the requester has enough permission. Answer forbidden error if
                // requester permissions are not enough to execute the requested command
                if (!command.hasPermission(packet.getFrom())) {
                    reply.setChildElement(iqCommand.createCopy());
                    reply.setError(PacketError.Condition.forbidden);
                    return reply;
                }

                // Create new session ID
                sessionid = StringUtils.randomString(15);

                Element childElement = reply.setChildElement("command", NAMESPACE);

                if (command.getMaxStages(null) == 0) {
                    // The command does not require any user interaction (returns results only)
                    // Execute the command and return the execution result which may be a
                    // data form (i.e. report data) or a note element
                    command.execute(null, childElement);
                    childElement.addAttribute("sessionid", sessionid);
                    childElement.addAttribute("node", commandCode);
                    childElement.addAttribute("status", AdHocCommand.Status.completed.name());
                }
                else {
                    // The command requires user interactions (ie. has stages)

                    // Clean up expired sessions data
                    Collection<String> sessionsIDs = sessionsCounter.get(from);
                    if (sessionsIDs != null) {
                        synchronized (from.intern()) {
                            Collection<String> existingSessionsIDs = new ArrayList<String>(sessionsIDs);
                            for (String existingSessionID : existingSessionsIDs) {
                                SessionData session = sessions.get(existingSessionID);
                                // Check if the Session data has expired (default is 10 minutes)
                                expireSessionData(session, from);
                            }
                        }
                    }

                    // Check that the user has not exceeded the limit of allowed simultaneous
                    // command sessions.
                    sessionsIDs = sessionsCounter.get(from);
                    if (sessionsIDs == null) {
                        synchronized (from.intern()) {
                            sessionsIDs = sessionsCounter.get(from);
                            if (sessionsIDs == null) {
                                sessionsIDs = new ArrayList<String>();
                                sessionsCounter.put(from, sessionsIDs);
                            }
                        }
                    }
                    int limit = JiveGlobals.getIntProperty("xmpp.command.limit", 100);
                    if (sessionsIDs.size() > limit) {
                        // Answer a not_allowed error since the user has exceeded limit. This
                        // checking prevents bad users from consuming all the system memory by not
                        // allowing them to create infinite simultaneous command sessions.
                        reply.setChildElement(iqCommand.createCopy());
                        reply.setError(PacketError.Condition.not_allowed);
                        return reply;
                    }
                    // Keep track of sessions per XMPP entity
                    synchronized (from.intern()) {
                        sessionsIDs.add(sessionid);
                    }
                    // Originate a new command session.
                    SessionData session = new SessionData(sessionid, packet.getFrom());
                    sessions.put(sessionid, session);

                    childElement.addAttribute("sessionid", sessionid);
                    childElement.addAttribute("node", commandCode);
                    childElement.addAttribute("status", AdHocCommand.Status.executing.name());

                    // Add to the child element the data form the user must complete and
                    // the allowed actions
                    command.addNextStageInformation(session, childElement);
                }
            }

        }
        else {
            // An execution session already exists and the user has requested to perform a
            // certain action.
            String action = iqCommand.attributeValue("action");
            SessionData session = sessions.get(sessionid);
            // Check that a Session exists for the specified sessionID
            if (session == null) {
                // Answer a bad_request error (bad-sessionid)
                reply.setChildElement(iqCommand.createCopy());
                reply.setError(PacketError.Condition.bad_request);
                return reply;
            }

            // Check if the Session data has expired (default is 10 minutes)
            if (expireSessionData(session, from)) {
                // Answer a not_allowed error (session-expired)
                reply.setChildElement(iqCommand.createCopy());
                reply.setError(PacketError.Condition.not_allowed);
                return reply;
            }

            synchronized (sessionid.intern()) {
                // Check if the user is requesting to cancel the command
                if (AdHocCommand.Action.cancel.name().equals(action)) {
                    // User requested to cancel command execution so remove the session data
                    removeSessionData(sessionid, from);
                    // Generate a canceled confirmation response
                    Element childElement = reply.setChildElement("command", NAMESPACE);
                    childElement.addAttribute("sessionid", sessionid);
                    childElement.addAttribute("node", commandCode);
                    childElement.addAttribute("status", AdHocCommand.Status.canceled.name());
                }

                // If the user didn't specify an action then follow the default execute action
                if (action == null || AdHocCommand.Action.execute.name().equals(action)) {
                    action = session.getExecuteAction().name();
                }

                // Check that the specified action was previously offered
                if (!session.isValidAction(action)) {
                    // Answer a bad_request error (bad-action)
                    reply.setChildElement(iqCommand.createCopy());
                    reply.setError(PacketError.Condition.bad_request);
                    return reply;
                }
                else if (AdHocCommand.Action.prev.name().equals(action)) {
                    // Move to the previous stage and add to the child element the data form
                    // the user must complete and the allowed actions of the previous stage
                    Element childElement = reply.setChildElement("command", NAMESPACE);
                    childElement.addAttribute("sessionid", sessionid);
                    childElement.addAttribute("node", commandCode);
                    childElement.addAttribute("status", AdHocCommand.Status.executing.name());
                    command.addPreviousStageInformation(session, childElement);
                }
                else if (AdHocCommand.Action.next.name().equals(action)) {
                    // Store the completed form in the session data
                    saveCompletedForm(iqCommand, session);
                    // Move to the next stage and add to the child element the new data form
                    // the user must complete and the new allowed actions
                    Element childElement = reply.setChildElement("command", NAMESPACE);
                    childElement.addAttribute("sessionid", sessionid);
                    childElement.addAttribute("node", commandCode);
                    childElement.addAttribute("status", AdHocCommand.Status.executing.name());
                    command.addNextStageInformation(session, childElement);
                }
                else if (AdHocCommand.Action.complete.name().equals(action)) {
                    // Store the completed form in the session data
                    saveCompletedForm(iqCommand, session);
                    // Execute the command and return the execution result which may be a
                    // data form (i.e. report data) or a note element
                    Element childElement = reply.setChildElement("command", NAMESPACE);
                    command.execute(session, childElement);
                    childElement.addAttribute("sessionid", sessionid);
                    childElement.addAttribute("node", commandCode);
                    childElement.addAttribute("status", AdHocCommand.Status.completed.name());

                    // Command has been executed so remove the session data
                    removeSessionData(sessionid, from);
                }
            }
        }
        return reply;
    }

    /**
     * Stores in the SessionData the fields and their values as specified in the completed
     * data form by the user.
     *
     * @param iqCommand the command element containing the data form element.
     * @param session the SessionData for this command execution.
     */
    private void saveCompletedForm(Element iqCommand, SessionData session) {
        Element formElement = iqCommand.element(QName.get("x", "jabber:x:data"));
        if (formElement != null) {
            // Generate a Map with the variable names and variables values
            Map<String, List<String>> data = new HashMap<String, List<String>>();
            DataForm dataForm = new DataForm(formElement);
            for (FormField field : dataForm.getFields()) {
                data.put(field.getVariable(), field.getValues());
            }
            // Store the variables and their values in the session data
            session.addStageForm(data);
        }
    }

    /**
     * Releases the data kept for the command execution whose id is sessionid. The number of
     * commands executions currently being executed by the user (full JID) will be decreased.
     *
     * @param sessionid id of the session that identifies this command execution.
     * @param from the full JID of the command requester.
     */
    private void removeSessionData(String sessionid, String from) {
        sessions.remove(sessionid);
        synchronized (from.intern()) {
            Collection<String> sessionsIDs = sessionsCounter.get(from);
            sessionsIDs.remove(sessionid);
            if (sessionsIDs.isEmpty()) {
            // Remove the Collection when no commands are being executed for this XMPP entity
            sessionsCounter.remove(from);
            }
        }
    }

    private void removeSessionsData(String from) {
        synchronized (from.intern()) {
            Collection<String> sessionsIDs = sessionsCounter.get(from);
            if (sessionsIDs == null) {
                // Nothing to clean up since there are no ad-hoc commands being executed
                // from this address at this time
                return;
            }
            for (String sessionid : sessionsIDs) {
                sessions.remove(sessionid);
            }
        }
        sessionsCounter.remove(from);
    }

    public void stop() {
        // Cancel executions of running commands
        sessions.clear();
        sessionsCounter.clear();
    }

    private boolean expireSessionData(SessionData session, String from) {
        int timeout = JiveGlobals.getIntProperty("xmpp.command.timeout", 10 * 60 * 1000);
        if (System.currentTimeMillis() - session.getCreationStamp() > timeout) {
            // Remove the old session
            removeSessionData(session.getId(), from);
            return true;
        }
        return false;
    }
    /***************** Session Listeners **********************/
    public void componentRegistered(JID componentJID) {
        //Ignore
    }

    public void componentUnregistered(JID componentJID) {
        removeSessionsData(componentJID.toString());
    }

    public void componentInfoReceived(IQ iq) {
        //Ignore
    }

    public void sessionCreated(Session session) {
        //Ignore
    }

    public void sessionDestroyed(Session session) {
        removeSessionsData(session.getAddress().toString());
    }

    public void anonymousSessionCreated(Session session) {
        //Ignore
    }

    public void anonymousSessionDestroyed(Session session) {
        removeSessionsData(session.getAddress().toString());
    }

    public void resourceBound(Session session) {
        //Ignore
    }

    public void init() {
        InternalComponentManager.getInstance().addListener(this);
        SessionEventDispatcher.addListener(this);
    }
}
