/*
 * RHQ Management Platform
 * Copyright (C) 2005-2008 Red Hat, Inc.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package org.rhq.enterprise.server.core;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.interceptor.ExcludeDefaultInterceptors;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.maven.artifact.versioning.ComparableVersion;

import org.jboss.annotation.IgnoreDependency;

import org.rhq.core.clientapi.server.core.AgentVersion;
import org.rhq.core.domain.auth.Subject;
import org.rhq.core.domain.authz.Permission;
import org.rhq.core.domain.measurement.AvailabilityType;
import org.rhq.core.domain.resource.Agent;
import org.rhq.core.domain.resource.composite.AgentLastAvailabilityReportComposite;
import org.rhq.core.domain.util.PageControl;
import org.rhq.core.domain.util.PageList;
import org.rhq.core.server.PersistenceUtility;
import org.rhq.core.util.MessageDigestGenerator;
import org.rhq.core.util.stream.StreamUtil;
import org.rhq.enterprise.server.RHQConstants;
import org.rhq.enterprise.server.agentclient.AgentClient;
import org.rhq.enterprise.server.authz.RequiredPermission;
import org.rhq.enterprise.server.cloud.FailoverListManagerLocal;
import org.rhq.enterprise.server.core.comm.ServerCommunicationsServiceMBean;
import org.rhq.enterprise.server.core.comm.ServerCommunicationsServiceUtil;
import org.rhq.enterprise.server.measurement.AvailabilityManagerLocal;
import org.rhq.enterprise.server.system.SystemManagerLocal;
import org.rhq.enterprise.server.util.LookupUtil;
import org.rhq.enterprise.server.util.concurrent.AvailabilityReportSerializer;

/**
 * Manages the access to {@link Agent} objects.<br>
 * <br>
 * Some of these methods need to execute as fast as possible. So, @ExcludeDefaultInterceptors has been added
 * to all methods that don't explicitly need a permission check (those without Subject as the first parameter).
 *
 * @author John Mazzitelli
 */
@Stateless
public class AgentManagerBean implements AgentManagerLocal {
    private final Log log = LogFactory.getLog(AgentManagerBean.class);

    @PersistenceContext(unitName = RHQConstants.PERSISTENCE_UNIT_NAME)
    private EntityManager entityManager;

    @EJB
    @IgnoreDependency
    private FailoverListManagerLocal failoverListManager;

    @EJB
    @IgnoreDependency
    private AvailabilityManagerLocal availabilityManager;

    @EJB
    private AgentManagerLocal agentManager;

    @EJB
    private SystemManagerLocal systemManager;

    // constants used for the agent update version file
    private static final String RHQ_SERVER_VERSION = "rhq-server.version";
    private static final String RHQ_SERVER_BUILD_NUMBER = "rhq-server.build-number";
    private static final String RHQ_AGENT_LATEST_VERSION = "rhq-agent.latest.version";
    private static final String RHQ_AGENT_LATEST_BUILD_NUMBER = "rhq-agent.latest.build-number";
    private static final String RHQ_AGENT_LATEST_MD5 = "rhq-agent.latest.md5";

    @ExcludeDefaultInterceptors
    public void createAgent(Agent agent) {
        entityManager.persist(agent);

        log.debug("Persisted new agent: " + agent);
    }

    @ExcludeDefaultInterceptors
    public void deleteAgent(Agent agent) {
        agent = entityManager.find(Agent.class, agent.getId());
        failoverListManager.deleteServerListsForAgent(agent);
        entityManager.remove(agent);

        ServerCommunicationsServiceMBean bootstrap = ServerCommunicationsServiceUtil.getService();
        try {
            bootstrap.destroyKnownAgentClient(agent);
        } catch (Exception e) {
            // certain unit tests won't create the agentClient
            log.warn("Could not find agentClient for doomedAgent: " + agent);
        }

        log.info("Removed agent: " + agent);
    }

    @ExcludeDefaultInterceptors
    public Agent updateAgent(Agent agent) {
        agent = entityManager.merge(agent);
        log.debug("Updated agent: " + agent);
        return agent;
    }

    @ExcludeDefaultInterceptors
    public AgentClient getAgentClient(Agent agent) {
        AgentClient client = null;

        try {
            ServerCommunicationsServiceMBean bootstrap = ServerCommunicationsServiceUtil.getService();
            client = bootstrap.getKnownAgentClient(agent);

            // We assume the caller is asking for a client so it can send the agent messages,
            // so let's start the sender automatically for the caller so it doesn't need to remember to do it
            client.startSending();
        } catch (Throwable t) {
            log.debug("Could not get agent client for " + agent);
        }

        return client;
    }

    @ExcludeDefaultInterceptors
    public AgentClient getAgentClient(int resourceId) {
        Agent agent = getAgentByResourceId(resourceId);

        if (agent == null) {
            log.debug("Resource [" + resourceId + "] does not exist or has no agent assigned");
            return null;
        }

        return getAgentClient(agent);
    }

    @ExcludeDefaultInterceptors
    public void agentIsShuttingDown(String agentName) {
        Agent downedAgent = getAgentByName(agentName);

        ServerCommunicationsServiceMBean server_bootstrap = ServerCommunicationsServiceUtil.getService();
        server_bootstrap.removeDownedAgent(downedAgent.getRemoteEndpoint());
        log.info("Agent with name [" + agentName + "] just went down");

        return;
    }

    @ExcludeDefaultInterceptors
    public void agentIsAlive(Agent agent) {
        // This method needs to be very fast.  It is currently designed to be called from
        // the security token command authenticator.  It calls this method every time
        // a message comes in and the auth token needs to be verified.  This method takes
        // a detached agent rather than an agent name because I don't want to waste a round
        // trip looking up the agent from the DB when I already know the caller (the
        // authenticator) just got the agent object.
        // When the authenticator calls us, it means it just got a message from the agent,
        // so we can use this to our benefit.  Since we got a message from the given agent,
        // we know it is up!  So this is a simple way for us to detect agents coming online
        // without us having to periodically poll each and every agent.

        try {
            ServerCommunicationsServiceMBean server_comm = ServerCommunicationsServiceUtil.getService();
            server_comm.addStartedAgent(agent);
        } catch (Exception e) {
            log.info("Cannot flag the agent as started for some reason", e);
        }

        return;
    }

    @SuppressWarnings("unchecked")
    @ExcludeDefaultInterceptors
    public void checkForSuspectAgents() {
        log.debug("Checking to see if there are agents that we suspect are down...");

        long maximumQuietTimeAllowed = 900000L;
        try {
            String prop = systemManager.getSystemConfiguration().getProperty(RHQConstants.AgentMaxQuietTimeAllowed);
            if (prop != null) {
                maximumQuietTimeAllowed = Long.parseLong(prop);
            }
        } catch (Exception e) {
            log.warn("Agent quiet time config is invalid in DB, defaulting to: " + maximumQuietTimeAllowed, e);
        }

        List<AgentLastAvailabilityReportComposite> records;

        long nowEpoch = System.currentTimeMillis();

        Query q = entityManager.createNamedQuery(Agent.QUERY_FIND_ALL_SUSPECT_AGENTS);
        q.setParameter("dateThreshold", nowEpoch - maximumQuietTimeAllowed);
        records = q.getResultList();

        ServerCommunicationsServiceMBean serverComm = null;

        for (AgentLastAvailabilityReportComposite record : records) {
            long lastReport = record.getLastAvailabilityReport();
            long timeSinceLastReport = nowEpoch - lastReport;

            // Only show this message a few times so we do not flood the log with the same message if the agent is down a long time
            // we show it as soon as we detect it going down (within twice max quiet time allowed) or we show it
            // after every 6th hour it's detected to be down (again, within twice max quiet time).  Effectively, you'll see
            // this message appear about 4 times per 6-hours for a downed agent if using the default max quiet time.
            // Note that in here we also make sure the agent client is shutdown. We do it here because, even if the agent
            // was already backfilled, we still want to do this in case somehow the client was started again.
            if ((timeSinceLastReport % 21600000L) < (maximumQuietTimeAllowed * 2L)) {
                if (serverComm == null) {
                    serverComm = ServerCommunicationsServiceUtil.getService();
                }

                serverComm.removeDownedAgent(record.getRemoteEndpoint());
            }

            // we can avoid doing this over and over again for agents that are down a long time by seeing if it's
            // already backfilled.  Note that we do not log the above warn message down here in this if-statement,
            // because I think we still want to log that we think an agent is down periodically.
            // If it turns out we do not want to be that noisy, just move that warn message down in here so we only ever log
            // about a downed agent once, at the time it is first backfilled.
            if (!record.isBackFilled()) {
                log.info("Have not heard from agent [" + record.getAgentName() + "] since ["
                    + new Date(record.getLastAvailabilityReport())
                    + "]. Will be backfilled since we suspect it is down");

                // make sure we lock out all processing of any availability reports that might come our way to avoid concurrency problems
                AvailabilityReportSerializer.getSingleton().lock(record.getAgentName());
                try {
                    agentManager.setAgentBackfilled(record.getAgentId(), true);
                    availabilityManager.setAllAgentResourceAvailabilities(record.getAgentId(), AvailabilityType.DOWN);
                } finally {
                    AvailabilityReportSerializer.getSingleton().unlock(record.getAgentName());
                }
            }
        }

        log.debug("Finished checking for suspected agents");

        return;
    }

    @SuppressWarnings("unchecked")
    @ExcludeDefaultInterceptors
    public List<Agent> getAllAgents() {
        return entityManager.createNamedQuery(Agent.QUERY_FIND_ALL).getResultList();
    }

    /** Methods with page control are typically accessed by the GUI, as such apply permission check. */
    @SuppressWarnings("unchecked")
    @RequiredPermission(Permission.MANAGE_INVENTORY)
    public PageList<Agent> getAgentsByServer(Subject subject, Integer serverId, PageControl pageControl) {
        pageControl.initDefaultOrderingField("a.name");

        Query query = PersistenceUtility.createQueryWithOrderBy(entityManager, Agent.QUERY_FIND_BY_SERVER, pageControl);
        Query countQuery = PersistenceUtility.createCountQuery(entityManager, Agent.QUERY_FIND_BY_SERVER);

        query.setParameter("serverId", serverId);
        countQuery.setParameter("serverId", serverId);

        long count = (Long) countQuery.getSingleResult();
        List<Agent> results = query.getResultList();

        return new PageList<Agent>(results, (int) count, pageControl);
    }

    @ExcludeDefaultInterceptors
    public int getAgentCount() {
        return ((Number) entityManager.createNamedQuery(Agent.QUERY_COUNT_ALL).getSingleResult()).intValue();
    }

    @ExcludeDefaultInterceptors
    public Agent getAgentByAgentToken(String token) {
        Agent agent;

        try {
            Query query = entityManager.createNamedQuery(Agent.QUERY_FIND_BY_AGENT_TOKEN);
            query.setParameter("agentToken", token);
            agent = (Agent) query.getSingleResult();
        } catch (NoResultException e) {
            log.debug("Failed to lookup agent - none exist with token [" + token + "] : " + e);
            agent = null;
        }

        return agent;
    }

    @ExcludeDefaultInterceptors
    public Agent getAgentByName(String agentName) {
        Agent agent;

        try {
            Query query = entityManager.createNamedQuery(Agent.QUERY_FIND_BY_NAME);
            query.setParameter("name", agentName);
            agent = (Agent) query.getSingleResult();
        } catch (NoResultException e) {
            log.debug("Failed to lookup agent - none exist with name [" + agentName + "] : " + e);
            agent = null;
        }

        return agent;
    }

    @ExcludeDefaultInterceptors
    public Agent getAgentByID(int agentId) {
        Agent agent = entityManager.find(Agent.class, agentId);
        return agent;
    }

    @ExcludeDefaultInterceptors
    public Agent getAgentByAddressAndPort(String address, int port) {
        Agent agent;
        try {
            Query query = entityManager.createNamedQuery(Agent.QUERY_FIND_BY_ADDRESS_AND_PORT);
            query.setParameter("address", address);
            query.setParameter("port", port);
            agent = (Agent) query.getSingleResult();
        } catch (NoResultException e) {
            log.debug("Agent not found with address/port: " + address + "/" + port);
            agent = null;
        }

        return agent;
    }

    @ExcludeDefaultInterceptors
    public Agent getAgentByResourceId(int resourceId) {
        Agent agent;

        try {
            Query query = entityManager.createNamedQuery(Agent.QUERY_FIND_BY_RESOURCE_ID);
            query.setParameter("resourceId", resourceId);
            agent = (Agent) query.getSingleResult();
        } catch (NoResultException e) {
            log.debug("Failed to lookup agent for resource with ID of [" + resourceId + "] : " + e);
            agent = null;
        }

        return agent;
    }

    @ExcludeDefaultInterceptors
    public Integer getAgentIdByResourceId(int resourceId) {
        Integer agentId;

        try {
            Query query = entityManager.createNamedQuery(Agent.QUERY_FIND_AGENT_ID_BY_RESOURCE_ID);
            query.setParameter("resourceId", resourceId);
            agentId = (Integer) query.getSingleResult();
        } catch (NoResultException e) {
            log.debug("Failed to lookup agent for resource with ID of [" + resourceId + "] : " + e);
            agentId = null;
        }

        return agentId;
    }

    @ExcludeDefaultInterceptors
    public Integer getAgentIdByName(String agentName) {
        Integer agentId;

        try {
            Query query = entityManager.createNamedQuery(Agent.QUERY_FIND_AGENT_ID_BY_NAME);
            query.setParameter("name", agentName);
            agentId = (Integer) query.getSingleResult();
        } catch (NoResultException e) {
            log.debug("Failed to lookup agent for name of [" + agentName + "] : " + e);
            agentId = null;
        }

        return agentId;
    }

    @ExcludeDefaultInterceptors
    public Integer getAgentIdByScheduleId(int scheduleId) {
        Integer agentId;

        try {
            Query query = entityManager.createNamedQuery(Agent.QUERY_FIND_AGENT_ID_BY_SCHEDULE_ID);
            query.setParameter("scheduleId", scheduleId);
            agentId = (Integer) query.getSingleResult();
        } catch (NoResultException e) {
            log.debug("Failed to lookup agent for resource with ID of [" + scheduleId + "] : " + e);
            agentId = null;
        }

        return agentId;
    }

    @ExcludeDefaultInterceptors
    public boolean isAgentVersionSupported(AgentVersion agentVersionInfo) {
        try {
            Properties properties = getAgentUpdateVersionFileContent();

            // Prime Directive: whatever agent update the server has installed is the one we support,
            // so both the version AND build number must match.
            // For developers, however, we want to allow to be less strict - only version needs to match.
            String supportedAgentVersion = properties.getProperty(RHQ_AGENT_LATEST_VERSION);
            if (supportedAgentVersion == null) {
                throw new NullPointerException("no agent version in file");
            }
            ComparableVersion agent = new ComparableVersion(agentVersionInfo.getVersion());
            ComparableVersion server = new ComparableVersion(supportedAgentVersion);
            if (Boolean.getBoolean("rhq.server.agent-update.nonstrict-version-check")) {
                return agent.equals(server);
            } else {
                String supportedAgentBuild = properties.getProperty(RHQ_AGENT_LATEST_BUILD_NUMBER);
                return agent.equals(server) && agentVersionInfo.getBuild().equals(supportedAgentBuild);
            }
        } catch (Exception e) {
            log.warn("Cannot determine if agent version [" + agentVersionInfo + "] is supported. Cause: " + e);
            return false; // assume we can't talk to it
        }
    }

    @ExcludeDefaultInterceptors
    public File getAgentUpdateVersionFile() throws Exception {
        File agentDownloadDir = getAgentDownloadDir();
        File versionFile = new File(agentDownloadDir, "rhq-server-agent-versions.properties");
        if (!versionFile.exists()) {
            // we do not have the version properties file yet, let's extract some info and create one
            StringBuilder serverVersionInfo = new StringBuilder();

            // first, get the server version info (by asking our server for the info)
            CoreServerMBean coreServer = LookupUtil.getCoreServer();
            serverVersionInfo.append(RHQ_SERVER_VERSION + '=').append(coreServer.getVersion()).append('\n');
            serverVersionInfo.append(RHQ_SERVER_BUILD_NUMBER + '=').append(coreServer.getBuildNumber()).append('\n');

            // calculate the MD5 of the agent update binary file
            File binaryFile = getAgentUpdateBinaryFile();
            String md5Property = RHQ_AGENT_LATEST_MD5 + '=' + MessageDigestGenerator.getDigestString(binaryFile) + '\n';

            // second, get the agent version info (by peeking into the agent update binary jar)
            JarFile binaryJarFile = new JarFile(binaryFile);
            JarEntry binaryJarFileEntry = binaryJarFile.getJarEntry("rhq-agent-update-version.properties");
            InputStream binaryJarFileEntryStream = binaryJarFile.getInputStream(binaryJarFileEntry);

            // now write the server and agent version info in our internal version file our servlet will use
            FileOutputStream versionFileOutputStream = new FileOutputStream(versionFile);
            try {
                versionFileOutputStream.write(serverVersionInfo.toString().getBytes());
                versionFileOutputStream.write(md5Property.getBytes());
                StreamUtil.copy(binaryJarFileEntryStream, versionFileOutputStream, false);
            } finally {
                try {
                    versionFileOutputStream.close();
                } catch (Exception e) {
                }
                try {
                    binaryJarFileEntryStream.close();
                } catch (Exception e) {
                }
            }
        }

        return versionFile;
    }

    @ExcludeDefaultInterceptors
    public Properties getAgentUpdateVersionFileContent() throws Exception {
        FileInputStream stream = new FileInputStream(getAgentUpdateVersionFile());
        try {
            Properties props = new Properties();
            props.load(stream);
            return props;
        } finally {
            try {
                stream.close();
            } catch (Exception e) {
            }
        }
    }

    @ExcludeDefaultInterceptors
    public File getAgentUpdateBinaryFile() throws Exception {
        File agentDownloadDir = getAgentDownloadDir();
        for (File file : agentDownloadDir.listFiles()) {
            if (file.getName().endsWith(".jar")) {
                return file;
            }
        }

        throw new FileNotFoundException("Missing agent update binary in [" + agentDownloadDir + "]");
    }

    @ExcludeDefaultInterceptors
    public File getAgentDownloadDir() throws Exception {
        File serverHomeDir = LookupUtil.getCoreServer().getJBossServerHomeDir();
        File agentDownloadDir = new File(serverHomeDir, "deploy/rhq.ear/rhq-downloads/rhq-agent");
        if (!agentDownloadDir.exists()) {
            throw new FileNotFoundException("Missing agent downloads directory at [" + agentDownloadDir + "]");
        }
        return agentDownloadDir;
    }

    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public void setAgentBackfilled(int agentId, boolean backfilled) {
        Query query = entityManager.createNamedQuery(Agent.QUERY_SET_AGENT_BACKFILLED);
        query.setParameter("agentId", agentId);
        query.setParameter("backfilled", backfilled);
        query.executeUpdate();
    }

    public boolean isAgentBackfilled(int agentId) {
        // query returns 0 if the agent's platform is DOWN (or does not exist), 1 if not
        Query query = entityManager.createNamedQuery(Agent.QUERY_IS_AGENT_BACKFILLED);
        query.setParameter("agentId", agentId);
        Long backfilledCount = (Long) query.getSingleResult();
        return backfilledCount != 0L;
    }
}