// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you 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 com.cloud.agent.manager;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.inject.Inject;

import org.apache.log4j.Logger;
import org.springframework.stereotype.Component;

import com.cloud.agent.AgentManager;
import com.cloud.agent.Listener;
import com.cloud.agent.api.AgentControlAnswer;
import com.cloud.agent.api.AgentControlCommand;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.Command;
import com.cloud.agent.api.PingCommand;
import com.cloud.agent.api.StartupCommand;
import com.cloud.alert.AlertManager;
import com.cloud.configuration.dao.ConfigurationDao;
import com.cloud.dc.DataCenterVO;
import com.cloud.dc.HostPodVO;
import com.cloud.dc.dao.ClusterDao;
import com.cloud.dc.dao.DataCenterDao;
import com.cloud.dc.dao.HostPodDao;
import com.cloud.host.Host;
import com.cloud.host.HostVO;
import com.cloud.host.Status;
import com.cloud.host.Status.Event;
import com.cloud.host.dao.HostDao;
import com.cloud.resource.ResourceManager;
import com.cloud.resource.ResourceState;
import com.cloud.utils.db.ConnectionConcierge;
import com.cloud.utils.db.DB;
import com.cloud.utils.db.SearchCriteria2;
import com.cloud.utils.db.SearchCriteria.Op;
import com.cloud.utils.db.SearchCriteriaService;
import com.cloud.utils.time.InaccurateClock;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.dao.VMInstanceDao;

@Component
public class AgentMonitor extends Thread implements AgentMonitorService {
    private static Logger s_logger = Logger.getLogger(AgentMonitor.class);
    private static Logger status_Logger = Logger.getLogger(Status.class);
    private long _pingTimeout = 120; // Default set to 120 seconds
    @Inject private HostDao _hostDao;
    private boolean _stop;
    @Inject private AgentManager _agentMgr;
    @Inject private VMInstanceDao _vmDao;
    @Inject private DataCenterDao _dcDao = null;
    @Inject private HostPodDao _podDao = null;
    @Inject private AlertManager _alertMgr;
    private long _msId;
    @Inject ClusterDao _clusterDao;
    @Inject ResourceManager _resourceMgr;
        
    // private ConnectionConcierge _concierge;
    private Map<Long, Long> _pingMap;

    public AgentMonitor() {
        _pingMap = new ConcurrentHashMap<Long, Long>(10007);
    }
    
    /**
     * Check if the agent is behind on ping
     *
     * @param agentId
     *            agent or host id.
     * @return null if the agent is not kept here. true if behind; false if not.
     */
    public Boolean isAgentBehindOnPing(long agentId) {
        Long pingTime = _pingMap.get(agentId);
        if (pingTime == null) {
            return null;
        }
        return pingTime < (InaccurateClock.getTimeInSeconds() - _pingTimeout);
    }

    public Long getAgentPingTime(long agentId) {
        return _pingMap.get(agentId);
    }

    public void pingBy(long agentId) {
        _pingMap.put(agentId, InaccurateClock.getTimeInSeconds());
    }

    // TODO : use host machine time is not safe in clustering environment
    @Override
    public void run() {
        s_logger.info("Agent Monitor is started.");

        while (!_stop) {
            try {
                // check every 60 seconds
                Thread.sleep(60 * 1000);
            } catch (InterruptedException e) {
                s_logger.info("Who woke me from my slumber?");
            }

            try {
                List<Long> behindAgents = findAgentsBehindOnPing();
                for (Long agentId : behindAgents) {
                	SearchCriteriaService<HostVO, HostVO> sc = SearchCriteria2.create(HostVO.class);
                	sc.addAnd(sc.getEntity().getId(), Op.EQ, agentId);
                	HostVO h = sc.find();
                    if (h != null) {
                        ResourceState resourceState = h.getResourceState();
                        if (resourceState == ResourceState.Disabled || resourceState == ResourceState.Maintenance
                                || resourceState == ResourceState.ErrorInMaintenance) {
                            /*
                             * Host is in non-operation state, so no
                             * investigation and direct put agent to
                             * Disconnected
                             */
                            status_Logger.debug("Ping timeout but host " + agentId + " is in resource state of "
                                    + resourceState + ", so no investigation");
                            _agentMgr.disconnectWithoutInvestigation(agentId, Event.ShutdownRequested);
                        } else {
                            status_Logger.debug("Ping timeout for host " + agentId + ", do invstigation");
                            _agentMgr.disconnectWithInvestigation(agentId, Event.PingTimeout);
                        }
                	}
                }

                SearchCriteriaService<HostVO, HostVO> sc = SearchCriteria2.create(HostVO.class);
                sc.addAnd(sc.getEntity().getResourceState(), Op.IN, ResourceState.PrepareForMaintenance, ResourceState.ErrorInMaintenance);
                List<HostVO> hosts = sc.list();

                for (HostVO host : hosts) {
                    long hostId = host.getId();
                    DataCenterVO dcVO = _dcDao.findById(host.getDataCenterId());
                    HostPodVO podVO = _podDao.findById(host.getPodId());
                    String hostDesc = "name: " + host.getName() + " (id:" + hostId + "), availability zone: " + dcVO.getName() + ", pod: " + podVO.getName();

                    if (host.getType() != Host.Type.Storage) {
                        List<VMInstanceVO> vos = _vmDao.listByHostId(hostId);
                        List<VMInstanceVO> vosMigrating = _vmDao.listVmsMigratingFromHost(hostId);
                        if (vos.isEmpty() && vosMigrating.isEmpty()) {
                            _alertMgr.sendAlert(AlertManager.ALERT_TYPE_HOST, host.getDataCenterId(), host.getPodId(), "Migration Complete for host " + hostDesc, "Host [" + hostDesc + "] is ready for maintenance");
                            _resourceMgr.resourceStateTransitTo(host, ResourceState.Event.InternalEnterMaintenance, _msId);
                        }
                    }
                }
            } catch (Throwable th) {
                s_logger.error("Caught the following exception: ", th);
            }
        }

        s_logger.info("Agent Monitor is leaving the building!");
    }

    public void signalStop() {
        _stop = true;
        interrupt();
    }

    @Override
    public boolean isRecurring() {
        return true;
    }

    @Override
    public boolean processAnswers(long agentId, long seq, Answer[] answers) {
        return false;
    }

    @Override @DB
    public boolean processCommands(long agentId, long seq, Command[] commands) {
        boolean processed = false;
        for (Command cmd : commands) {
            if (cmd instanceof PingCommand) {
                pingBy(agentId);
            }
        }
        return processed;
    }

    protected List<Long> findAgentsBehindOnPing() {
        List<Long> agentsBehind = new ArrayList<Long>();
        long cutoffTime = InaccurateClock.getTimeInSeconds() - _pingTimeout;
        for (Map.Entry<Long, Long> entry : _pingMap.entrySet()) {
            if (entry.getValue() < cutoffTime) {
                agentsBehind.add(entry.getKey());
            }
        }

        if (agentsBehind.size() > 0) {
            s_logger.info("Found the following agents behind on ping: " + agentsBehind);
        }

        return agentsBehind;
    }

    /**
     * @deprecated We're using the in-memory
     */
    @Deprecated
    protected List<HostVO> findHostsBehindOnPing() {
        long time = (System.currentTimeMillis() >> 10) - _pingTimeout;
        List<HostVO> hosts = _hostDao.findLostHosts(time);
        if (s_logger.isInfoEnabled()) {
            s_logger.info("Found " + hosts.size() + " hosts behind on ping. pingTimeout : " + _pingTimeout +
                    ", mark time : " + time);
        }

        for (HostVO host : hosts) {
            if (host.getType().equals(Host.Type.ExternalFirewall) ||
                    host.getType().equals(Host.Type.ExternalLoadBalancer) ||
                    host.getType().equals(Host.Type.TrafficMonitor) ||
                    host.getType().equals(Host.Type.SecondaryStorage)) {
                continue;
            }

            if (host.getManagementServerId() == null || host.getManagementServerId() == _msId) {
                if (s_logger.isInfoEnabled()) {
                    s_logger.info("Asking agent mgr to investgate why host " + host.getId() +
                            " is behind on ping. last ping time: " + host.getLastPinged());
                }
                _agentMgr.disconnectWithInvestigation(host.getId(), Event.PingTimeout);
            }
        }

        return hosts;
    }

    @Override
    public AgentControlAnswer processControlCommand(long agentId, AgentControlCommand cmd) {
        return null;
    }

    @Override
    public void processConnect(Host host, StartupCommand cmd, boolean forRebalance) {
        if (host.getType().equals(Host.Type.TrafficMonitor) ||
                host.getType().equals(Host.Type.SecondaryStorage)) {
            return;
        }

        // NOTE: We don't use pingBy here because we're initiating.
        _pingMap.put(host.getId(), InaccurateClock.getTimeInSeconds());
    }

    @Override
    public boolean processDisconnect(long agentId, Status state) {
        _pingMap.remove(agentId);
        return true;
    }

    @Override
    public boolean processTimeout(long agentId, long seq) {
        return true;
    }

    @Override
    public int getTimeout() {
        return -1;
    }

    @Override
    public void startMonitoring(long pingTimeout) {
        _pingTimeout = pingTimeout;
    	start();
    }
}

