/*
 * 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;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.database.SequenceManager;
import org.jivesoftware.openfire.group.Group;
import org.jivesoftware.xmpp.workgroup.utils.FastpathConstants;
import org.jivesoftware.xmpp.workgroup.utils.ModelUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.component.ComponentManagerFactory;
import org.xmpp.packet.JID;

/**
 * <p>Simple agent manager implementation without caching or intelligent/lazy loading of data.</p>
 * <p>This implementation loads all agents into memory so the number of agents is limited by the amount of
 * available heap. Our biggest customer estimates maybe 50 agents so this doesn't seem to be an unreasonable
 * implemenation strategy. Should be revisited when looking to scale beyond a few hundred agents.</p>
 *
 * @author Derek DeMoro
 */
public class AgentManager {

    private static final Logger Log = LoggerFactory.getLogger(AgentManager.class);

    private static final String LOAD_AGENTS =
            "SELECT agentID FROM fpAgent";
    private static final String INSERT_AGENT =
            "INSERT INTO fpAgent (agentID, agentJID, name, maxchats, minchats) VALUES (?,?,?,?,?)";
    private static final String DELETE_AGENT =
            "DELETE FROM fpAgent WHERE agentID=?";
    private static final String DELETE_AGENT_PROPS =
            "DELETE FROM fpAgentProp WHERE ownerID=?";

    private Map<String, Agent> agents = new HashMap<String, Agent>();

    public AgentManager() {
        loadAgents();
    }

    /**
     * Returns an agent based on it's <tt>JID</tt>.
     *
     * @param agentJID the <tt>JID</tt> of the agent.
     * @return an Agent
     * @throws AgentNotFoundException if the agent could not be loaded.
     */
    public Agent getAgent(JID agentJID) throws AgentNotFoundException {
        Agent agent = agents.get(agentJID.toBareJID());

        if (agent == null) {
            throw new AgentNotFoundException(agentJID.toBareJID());
        }
        return agent;
    }

    /**
     * Returns true if the specified agent exists.
     *
     * @param agentJID the <tt>JID</t> of the agent.
     * @return true if the agent exists, otherwise false.
     */
    public boolean hasAgent(JID agentJID) {
        try {
            getAgent(agentJID);
        }
        catch (AgentNotFoundException e) {
            return false;
        }
        return true;
    }

    /**
     * Returns an agent based on it's agent ID.
     *
     * @param agentID the agentID of the agent.
     * @return an agent.
     * @throws AgentNotFoundException if the agent could not be loaded. 
     */
    public Agent getAgent(long agentID) throws AgentNotFoundException {
        for (Agent agent : agents.values()) {
            if (agent.getID() == agentID) {
                return agent;
            }
        }

        throw new AgentNotFoundException(Long.toHexString(agentID));
    }

    /**
     * Returns the number of agents
     *
     * @return the total number of agents.
     */
    public int getAgentCount() {
        return agents.size();
    }

    public Collection<Agent> getAgents() {
        return Collections.unmodifiableList(new ArrayList<Agent>(agents.values()));
    }

    public Iterator<Agent> getAgents(WorkgroupResultFilter filter) {
        return filter.filter(agents.values().iterator());
    }

    /**
     * Returns the session of the requested agent. If a session is not found
     * then a <tt>null</tt> value will be return. If no agent was found for the
     * specified JID then an AgentNotFoundException will be thrown.
     *
     * @param agentJID the JID of the agent to look for an AgentSession.
     * @return the session of the requested agent in this workgroup.
     * @throws AgentNotFoundException If no agent was found for the specified JID.
     */
    public AgentSession getAgentSession(JID agentJID) throws AgentNotFoundException {
        return getAgent(agentJID).getAgentSession();
    }

    /**
     * Creates a new agent from an <code>XMPPAddress</code>
     *
     * @param agentJID the <code>XMPPAddress</code>
     * @return the <code>Agent</code> created.
     * @throws IllegalArgumentException if not a valid agent JID, such as a JID without a node.
     */
    public Agent createAgent(JID agentJID) throws IllegalArgumentException {
        if (!ModelUtil.hasLength((agentJID.getNode()))) {
            throw new IllegalArgumentException("No anonymous agents allowed");
        }

        // Get the next ID for workgroup agents.
        long agentID = SequenceManager.nextID(FastpathConstants.WORKGROUP_AGENT);

        if (!insertAgent(agentID, agentJID)) {
            throw new IllegalArgumentException("Agent could not be created");
        }

        Agent agent = new Agent(agentID);
        agent.setAgentJID(agentJID);
        agent.setNickname(agentJID.getNode());
        agents.put(agentJID.toBareJID(), agent);
        return agent;
    }

    /**
     * Remove the specified agent.
     *
     * @param agentJID the jid of the agent to remove.
     * @throws IllegalArgumentException if not a valid agent JID.
     */
    public void deleteAgent(JID agentJID) throws IllegalArgumentException {
        Agent agent = agents.remove(agentJID.toBareJID());
        if (agent != null) {
            Connection con = null;
            PreparedStatement pstmt = null;
            try {
                con = DbConnectionManager.getConnection();
                pstmt = con.prepareStatement(DELETE_AGENT);
                pstmt.setLong(1, agent.getID());
                pstmt.executeUpdate();
                pstmt.close();

                pstmt = con.prepareStatement(DELETE_AGENT_PROPS);
                pstmt.setLong(1, agent.getID());
                pstmt.executeUpdate();
            }
            catch (SQLException sqle) {
                Log.error(sqle.getMessage(), sqle);
            }
            finally {
                DbConnectionManager.closeConnection(pstmt, con);
            }
        }
    }

    /**
     * Returns all agents belonging to a Shared Group.
     *
     * @param group the shared group.
     * @return the collection of agents.
     */
    public Collection<Agent> getAgents(Group group) {
        final Set<Agent> set = new HashSet<Agent>();
        for (JID jid : group.getMembers()) {
            Agent agent;
            try {
                agent = getAgent(jid);
            }
            catch (AgentNotFoundException e) {
                agent = createAgent(jid);
            }
            set.add(agent);
        }

        return set;
    }

    /**
     * Return true if the agent belongs to the given workgroup.
     *
     * @param agent     the agent.
     * @param workgroup the workgroup.
     * @return true if the agent is in the workgrouop.
     */
    public boolean isInWorkgroup(Agent agent, Workgroup workgroup) {
        boolean isMember = false;
        for (RequestQueue requestQueue : workgroup.getRequestQueues()) {
            if (!isMember) {
                isMember = requestQueue.isMember(agent);
            }
        }
        return isMember;
    }

    /**
     * Removes an agent from the system if they no longer belong to any Workgroups.
     *
     * @param agent the agent to remove.
     */
    public void removeAgentIfNecessary(Agent agent) {
        // Go through all workgroups to see if this user is in RequestQueues
        WorkgroupManager workgroupManager = WorkgroupManager.getInstance();
        for (Workgroup workgroup : workgroupManager.getWorkgroups()) {
            for (RequestQueue queue : workgroup.getRequestQueues()) {
                if (queue.getMembers().contains(agent)) {
                    return;
                }

                for (Group group : queue.getGroups()) {
                    if (group.isUser(agent.getAgentJID())) {
                        return;
                    }
                }
            }
        }

        try {
            // Send Presence to Workgroup that this user is no longer available.
            AgentSession session = agent.getAgentSession();
            if (session != null) {
                for (Workgroup workgroup : session.getWorkgroups()) {
                    session.depart(workgroup);
                }
            }

            // If we get here, then remove from agent.
            deleteAgent(agent.getAgentJID());
        }
        catch (IllegalArgumentException e) {
            Log.error(e.getMessage(), e);
        }
    }

    /**
     * Adds an agent to the Database.
     *
     * @param agentID  the id of the agent to add.
     * @param agentJID the jid of the agent.
     * @return true if the agent was added, otherwise false.
     */
    private boolean insertAgent(long agentID, JID agentJID) {
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(INSERT_AGENT);
            pstmt.setLong(1, agentID);
            // If the JID of the agent is of the local server then just store the username
            String hostname = ComponentManagerFactory.getComponentManager().getServerName();
            String agentBareJID = agentJID.toBareJID();
            if (hostname.equals(agentJID.getDomain())) {
                agentBareJID = agentJID.getNode();
            }
            pstmt.setString(2, agentBareJID);
            pstmt.setString(3, agentJID.getNode());
            pstmt.setInt(4, -1);
            pstmt.setInt(5, -1);
            pstmt.executeUpdate();
            return true;
        }
        catch (Exception ex) {
            Log.error(ex.getMessage(), ex);
        }
        finally {
            DbConnectionManager.closeConnection(pstmt, con);
        }
        return false;
    }

    private void loadAgents() {
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(LOAD_AGENTS);
            rs = pstmt.executeQuery();
            while (rs.next()) {
                final Agent agent = new Agent(rs.getLong(1));
                agents.put(agent.getAgentJID().toBareJID(), agent);
            }
        }
        catch (Exception ex) {
            Log.error(ex.getMessage(), ex);
        }
        finally {
            DbConnectionManager.closeConnection(rs, pstmt, con);
        }
    }
}
