/*******************************************************************************
 * This file is part of OpenNMS(R).
 *
 * Copyright (C) 2006-2011 The OpenNMS Group, Inc.
 * OpenNMS(R) is Copyright (C) 1999-2011 The OpenNMS Group, Inc.
 *
 * OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc.
 *
 * OpenNMS(R) 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, either version 3 of the License,
 * or (at your option) any later version.
 *
 * OpenNMS(R) 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 OpenNMS(R).  If not, see:
 *      http://www.gnu.org/licenses/
 *
 * For more information contact:
 *     OpenNMS(R) Licensing <license@opennms.org>
 *     http://www.opennms.org/
 *     http://www.opennms.com/
 *******************************************************************************/

package org.opennms.netmgt.collectd;

import java.io.File;
import java.util.Map;

import org.opennms.core.utils.InetAddressUtils;
import org.opennms.core.utils.ThreadCategory;
import org.opennms.netmgt.EventConstants;
import org.opennms.netmgt.collectd.Collectd.SchedulingCompletedFlag;
import org.opennms.netmgt.config.DataCollectionConfigFactory;
import org.opennms.netmgt.config.collector.CollectionSet;
import org.opennms.netmgt.config.collector.ServiceParameters;
import org.opennms.netmgt.dao.CollectorConfigDao;
import org.opennms.netmgt.dao.IpInterfaceDao;
import org.opennms.netmgt.eventd.EventIpcManagerFactory;
import org.opennms.netmgt.model.OnmsIpInterface;
import org.opennms.netmgt.model.RrdRepository;
import org.opennms.netmgt.model.events.EventBuilder;
import org.opennms.netmgt.scheduler.ReadyRunnable;
import org.opennms.netmgt.scheduler.Scheduler;
import org.opennms.netmgt.threshd.ThresholdingVisitor;
import org.springframework.transaction.PlatformTransactionManager;

/**
 * <P>
 * The CollectableService class ...
 * </P>
 * 
 * @author <A HREF="mailto:mike@opennms.org">Mike Davidson </A>
 * @author <A HREF="http://www.opennms.org/">OpenNMS </A>
 * 
 */
final class CollectableService implements ReadyRunnable {
    /**
     * Interface's parent node identifier
     */
    private volatile int m_nodeId;

    /**
     * Last known/current status
     */
    private volatile int m_status;

    /**
     * The last time the collector was scheduled for collection.
     */
    private volatile long m_lastScheduledCollectionTime;

    /**
     * The scheduler for collectd
     */
    private final Scheduler m_scheduler;

    /**
     * Service updates
     */
    private final CollectorUpdates m_updates;

    /**
     * The thresholdvisitor for this collectable service; called 
     */
    private final ThresholdingVisitor m_thresholdVisitor;
    /**
     * 
     */
    private static final boolean ABORT_COLLECTION = true;

	private final CollectionSpecification m_spec;

	private final SchedulingCompletedFlag m_schedulingCompletedFlag;

	private volatile CollectionAgent m_agent;

	private final PlatformTransactionManager m_transMgr;

    private final IpInterfaceDao m_ifaceDao;

    private final ServiceParameters m_params;
    
    private final RrdRepository m_repository;
    /**
     * Constructs a new instance of a CollectableService object.
     *
     * @param iface The interface on which to collect data
     * @param spec
     *            The package containing parms for this collectable service.
     * @param ifaceDao a {@link org.opennms.netmgt.dao.IpInterfaceDao} object.
     * @param scheduler a {@link org.opennms.netmgt.scheduler.Scheduler} object.
     * @param schedulingCompletedFlag a {@link org.opennms.netmgt.collectd.Collectd.SchedulingCompletedFlag} object.
     * @param transMgr a {@link org.springframework.transaction.PlatformTransactionManager} object.
     */
    protected CollectableService(OnmsIpInterface iface, IpInterfaceDao ifaceDao, CollectionSpecification spec, Scheduler scheduler, SchedulingCompletedFlag schedulingCompletedFlag, PlatformTransactionManager transMgr) throws CollectionInitializationException {
        m_agent = DefaultCollectionAgent.create(iface.getId(), ifaceDao, transMgr);
        m_spec = spec;
        m_scheduler = scheduler;
        m_schedulingCompletedFlag = schedulingCompletedFlag;
        m_ifaceDao = ifaceDao;
        m_transMgr = transMgr;

        m_nodeId = iface.getNode().getId().intValue();
        m_status = ServiceCollector.COLLECTION_SUCCEEDED;

        m_updates = new CollectorUpdates();

        m_lastScheduledCollectionTime = 0L;
        
        m_spec.initialize(m_agent);
        
        Map<String, Object> roProps=m_spec.getReadOnlyPropertyMap();
        m_params=new ServiceParameters(roProps);
        m_repository=m_spec.getRrdRepository(m_params.getCollectionName());
        
        m_thresholdVisitor = ThresholdingVisitor.create(m_nodeId, getHostAddress(), m_spec.getServiceName(), m_repository,  roProps);

    }
    
    /**
     * <p>getAddress</p>
     *
     * @return a {@link java.lang.Object} object.
     */
    public Object getAddress() {
    	return m_agent.getAddress();
    }
    
    /**
     * <p>getSpecification</p>
     *
     * @return a {@link org.opennms.netmgt.collectd.CollectionSpecification} object.
     */
    public CollectionSpecification getSpecification() {
    	return m_spec;
    }

    /**
     * Returns node identifier
     *
     * @return a int.
     */
    public int getNodeId() {
        return m_nodeId;
    }

    /**
     * Returns the service name
     *
     * @return a {@link java.lang.String} object.
     */
    public String getServiceName() {
        return m_spec.getServiceName();
    }

    /**
     * Returns the package name
     *
     * @return a {@link java.lang.String} object.
     */
    public String getPackageName() {
        return m_spec.getPackageName();
    }

    /**
     * Returns updates object
     *
     * @return a {@link org.opennms.netmgt.collectd.CollectorUpdates} object.
     */
    public CollectorUpdates getCollectorUpdates() {
        return m_updates;
    }

	/**
	 * Uses the existing package name to try and re-obtain the package from the collectd config factory.
	 * Should be called when the collect config has been reloaded.
	 *
	 * @param collectorConfigDao a {@link org.opennms.netmgt.dao.CollectorConfigDao} object.
	 */
	public void refreshPackage(CollectorConfigDao collectorConfigDao) {
		m_spec.refresh(collectorConfigDao);
		if (m_thresholdVisitor != null)
		    m_thresholdVisitor.reloadScheduledOutages();
	}

    /** {@inheritDoc} */
    @Override
    public String toString() {
        return "CollectableService for service "+m_nodeId+':'+getAddress()+':'+getServiceName();
    }


    /**
     * This method is used to evaluate the status of this interface and service
     * pair. If it is time to run the collection again then a value of true is
     * returned. If the interface is not ready then a value of false is
     * returned.
     *
     * @return a boolean.
     */
    public boolean isReady() {
        boolean ready = false;

        if (!isSchedulingComplete())
            return false;

        if (m_spec.getInterval() < 1) {
            ready = true;
        } else {
            ready = ((m_spec.getInterval() - (System.currentTimeMillis() - m_lastScheduledCollectionTime)) < 1);
        }

        return ready;
    }

	private boolean isSchedulingComplete() {
		return m_schedulingCompletedFlag.isSchedulingCompleted();
    }

    /**
     * Generate event and send it to eventd via the event proxy.
     * 
     * uei Universal event identifier of event to generate.
     */
    private void sendEvent(String uei, String reason) {
        EventBuilder builder = new EventBuilder(uei, "OpenNMS.Collectd");
        builder.setNodeid(m_nodeId);
        builder.setInterface(m_agent.getInetAddress());
        builder.setService(m_spec.getServiceName());
        builder.setHost(InetAddressUtils.getLocalHostName());
        
        if (reason != null) {
            builder.addParam("reason", reason);
        }

        // Send the event
        try {
            EventIpcManagerFactory.getIpcManager().sendNow(builder.getEvent());

            if (log().isDebugEnabled()) {
                log().debug("sendEvent: Sent event " + uei + " for " + m_nodeId + "/" + getHostAddress() + "/" + getServiceName());
            }
        } catch (Throwable e) {
            log().error("Failed to send the event " + uei + " for interface " + getHostAddress() + ": " + e, e);
        }
    }

    private String getHostAddress() {
        return m_agent.getHostAddress();
    }

    /**
     * This is the main method of the class. An instance is normally enqueued on
     * the scheduler which checks its <code>isReady</code> method to determine
     * execution. If the instance is ready for execution then it is started with
     * it's own thread context to execute the query. The last step in the method
     * before it exits is to reschedule the interface.
     */
    public void run() {
        // Process any outstanding updates.
        if (processUpdates() == ABORT_COLLECTION) {
            log().debug("run: Aborting because processUpdates returned ABORT_COLLECTION (probably marked for deletion) for "+this);
            return;
        }

        // Update last scheduled poll time
        m_lastScheduledCollectionTime = System.currentTimeMillis();

        /*
         * Check scheduled outages to see if any apply indicating
         * that the collection should be skipped.
         */
        if (!m_spec.scheduledOutage(m_agent)) {
            try {
                doCollection();
                updateStatus(ServiceCollector.COLLECTION_SUCCEEDED, null);
            } catch (CollectionException e) {
                if (e instanceof CollectionWarning) {
                    log().warn(e.getMessage(), e);
                } else {
                    log().error(e.getMessage(), e);
                }
                updateStatus(ServiceCollector.COLLECTION_FAILED, e);
            } catch (Throwable e) {
                log().error(e.getMessage(), e);
                updateStatus(ServiceCollector.COLLECTION_FAILED, new CollectionException("Collection failed unexpectedly: " + e.getClass().getSimpleName() + ": " + e.getMessage(), e));
            }
        }
        
    	// Reschedule the service
        m_scheduler.schedule(m_spec.getInterval(), getReadyRunnable());
    }

    private void updateStatus(int status, CollectionException e) {
        // Any change in status?
        if (status != m_status) {
            // Generate data collection transition events
            if (log().isDebugEnabled()) {
                log().debug("run: change in collection status, generating event.");
            }
            
            String reason = null;
            if (e != null) {
                reason = e.getMessage();
            }

            // Send the appropriate event
            switch (status) {
            case ServiceCollector.COLLECTION_SUCCEEDED:
                sendEvent(EventConstants.DATA_COLLECTION_SUCCEEDED_EVENT_UEI, null);
                break;

            case ServiceCollector.COLLECTION_FAILED:
                sendEvent(EventConstants.DATA_COLLECTION_FAILED_EVENT_UEI, reason);
                break;

            default:
                break;
            }
        }

        // Set the new status
        m_status = status;
    }

        private BasePersister createPersister(ServiceParameters params, RrdRepository repository) {
            if (Boolean.getBoolean("org.opennms.rrd.storeByGroup")) {
                return new GroupPersister(params, repository);
            } else {
                return new OneToOnePersister(params, repository);
            }
        }

        /**
         * Perform data collection.
         */
	private void doCollection() throws CollectionException {
		log().info("run: starting new collection for " + getHostAddress() + "/" + m_spec.getServiceName() + "/" + m_spec.getPackageName());
		CollectionSet result = null;
		try {
		    result = m_spec.collect(m_agent);
		    if (result != null) {
                        Collectd.instrumentation().beginPersistingServiceData(m_nodeId, getHostAddress(), m_spec.getServiceName());
                        try {
                            BasePersister persister = createPersister(m_params, m_repository);
                            persister.setIgnorePersist(result.ignorePersist());
                            result.visit(persister);
                        } finally {
                            Collectd.instrumentation().endPersistingServiceData(m_nodeId, getHostAddress(), m_spec.getServiceName());
                        }
                        
                        /*
                         * Do the thresholding; this could be made more generic (listeners being passed the collectionset), but frankly, why bother?
                         * The first person who actually needs to configure that sort of thing on the fly can code it up.
                         */
                        if (m_thresholdVisitor != null) {
                            if (m_thresholdVisitor.isNodeInOutage()) {
                                log().info("run: the threshold processing will be skipped because the node " + m_nodeId + " is on a scheduled outage.");
                            } else if (m_thresholdVisitor.hasThresholds()) {
                                result.visit(m_thresholdVisitor);
                            }
                        }
                       
                        if (result.getStatus() == ServiceCollector.COLLECTION_SUCCEEDED) {
                            return;
                        } else {
                            throw new CollectionFailed(result.getStatus());
                        }
                    }
                } catch (CollectionException e) {
                    log().warn("run: failed collection for " + getHostAddress() + "/" + m_spec.getServiceName() + "/" + m_spec.getPackageName());
                    throw e;
		} catch (Throwable t) {
                    log().warn("run: failed collection for " + getHostAddress() + "/" + m_spec.getServiceName() + "/" + m_spec.getPackageName());
                    throw new CollectionException("An undeclared throwable was caught during data collection for interface " + getHostAddress() +"/"+ m_spec.getServiceName(), t);
		}
		log().info("run: finished collection for " + getHostAddress() + "/" + m_spec.getServiceName() + "/" + m_spec.getPackageName());
	}

	/**
     * Process any outstanding updates.
     * 
     * @return true if update indicates that collection should be aborted (for
     *         example due to deletion flag being set), false otherwise.
     */
    private boolean processUpdates() {
        // All update processing takes place within synchronized block
        // to ensure that no updates are missed.
        //
        synchronized (this) {
            if (!m_updates.hasUpdates())
                return !ABORT_COLLECTION;

            // Update: deletion flag
            //
            if (m_updates.isDeletionFlagSet()) {
                // Deletion flag is set, simply return without polling
                // or rescheduling this collector.
                //
                if (log().isDebugEnabled())
                    log().debug("Collector for  " + getHostAddress() + " is marked for deletion...skipping collection, will not reschedule.");

                return ABORT_COLLECTION;
            }

            OnmsIpInterface newIface = m_updates.isReinitializationNeeded();
			// Update: reinitialization flag
            //
            if (newIface != null) {
                // Reinitialization flag is set, call initialize() to
                // reinit the collector for this interface
                //
                if (log().isDebugEnabled())
                    log().debug("ReinitializationFlag set for " + getHostAddress());

                try {
                    reinitialize(newIface);
                    if (log().isDebugEnabled())
                        log().debug("Completed reinitializing "+this.getServiceName()+" collector for " + getHostAddress() +"/"+ m_spec.getServiceName());
                } catch (CollectionInitializationException rE) {
                    log().warn("Unable to initialize " + getHostAddress() + " for " + m_spec.getServiceName() + " collection, reason: " + rE.getMessage());
                } catch (Throwable t) {
                    log().error("Uncaught exception, failed to intialize interface " + getHostAddress() + " for " + m_spec.getServiceName() + " data collection", t);
                }
            }

            // Update: reparenting flag
            //
            if (m_updates.isReparentingFlagSet()) {
                if (log().isDebugEnabled())
                    log().debug("ReparentingFlag set for " + getHostAddress());

                // The interface has been reparented under a different node
                // (with
                // a different nodeId).
                //
                // If the new directory doesn't already exist simply need to
                // rename the old
                // directory:
                // /opt/OpenNMS/share/rrd/snmp/<oldNodeId>
                // to the new directory:
                // /opt/OpenNMS/share/rrd/snmp/<newNodeId>
                //
                // Otherwise must iterate over each of the files/dirs in the
                // <oldNodeId>
                // directory and move/rename them under the <newNodeId>
                // directory.

                // Get path to RRD repository
                //
                String rrdPath = DataCollectionConfigFactory.getInstance().getRrdPath();

                // Does the <newNodeId> directory already exist?
                File newNodeDir = new File(rrdPath + File.separator + m_updates.getReparentNewNodeId());
                if (!newNodeDir.isDirectory()) {
                    // New directory does not exist yet so simply rename the old
                    // directory to
                    // the new directory.
                    //

                    // <oldNodeId> directory
                    File oldNodeDir = new File(rrdPath + File.separator + m_updates.getReparentOldNodeId());

                    try {
                        // Rename <oldNodeId> dir to <newNodeId> dir.
                        if (log().isDebugEnabled())
                            log().debug("Attempting to rename " + oldNodeDir + " to " + newNodeDir);
                        oldNodeDir.renameTo(newNodeDir);
                        if (log().isDebugEnabled())
                            log().debug("Rename successful!!");
                    } catch (SecurityException se) {
                        log().error("Insufficient authority to rename RRD directory.", se);
                    } catch (Throwable t) {
                        log().error("Unexpected exception while attempting to rename RRD directory.", t);
                    }
                } else {
                    // New node directory already exists so we must move/rename
                    // each of the
                    // old node directory contents under the new node directory.
                    //

                    // Get list of files to be renamed/moved
                    File oldNodeDir = new File(rrdPath + File.separator + m_updates.getReparentOldNodeId());
                    String[] filesToMove = oldNodeDir.list();

                    if (filesToMove != null) {
                        // Iterate over the file list and rename/move each one
                        for (int i = 0; i < filesToMove.length; i++) {
                            File srcFile = new File(oldNodeDir.toString() + File.separator + filesToMove[i]);
                            File destFile = new File(newNodeDir.toString() + File.separator + filesToMove[i]);
                            try {
                                if (log().isDebugEnabled())
                                    log().debug("Attempting to move " + srcFile + " to " + destFile);
                                srcFile.renameTo(destFile);
                            } catch (SecurityException se) {
                                log().error("Insufficient authority to move RRD files.", se);
                                break;
                            } catch (Throwable t) {
                                log().warn("Unexpected exception while attempting to move " + srcFile + " to " + destFile, t);
                            }
                        }
                    }
                }

                // Convert new nodeId to integer value
                int newNodeId = -1;
                try {
                    newNodeId = Integer.parseInt(m_updates.getReparentNewNodeId());
                } catch (NumberFormatException nfE) {
                    log().warn("Unable to convert new nodeId value to an int while processing reparenting update: " + m_updates.getReparentNewNodeId());
                }

                // Set this collector's nodeId to the value of the interface's
                // new parent nodeid.
                m_nodeId = newNodeId;

                // We must now reinitialize the collector for this interface,
                // in order to update the NodeInfo object to reflect changes
                // to the interface's parent node among other things.
                //
                try {
                    if (log().isDebugEnabled())
                        log().debug("Reinitializing collector for " + getHostAddress() +"/"+ m_spec.getServiceName());
                    reinitialize(m_updates.getUpdatedInterface());
                    if (log().isDebugEnabled())
                        log().debug("Completed reinitializing collector for " + getHostAddress() +"/"+ m_spec.getServiceName());
                } catch (CollectionInitializationException rE) {
                    log().warn("Unable to initialize " + getHostAddress() + " for " + m_spec.getServiceName() + " collection, reason: " + rE.getMessage());
                } catch (Throwable t) {
                    log().error("Uncaught exception, failed to initialize interface " + getHostAddress() + " for " + m_spec.getServiceName() + " data collection", t);
                }
            }

            // Updates have been applied. Reset CollectorUpdates object.
            // .
            m_updates.reset();
        } // end synchronized

        return !ABORT_COLLECTION;
    }
    
    ThreadCategory log() {
    	return ThreadCategory.getInstance(getClass());
    }

    private void reinitialize(OnmsIpInterface newIface) throws CollectionInitializationException {
        m_spec.release(m_agent);
        m_agent = DefaultCollectionAgent.create(newIface.getId(), m_ifaceDao,
                                                m_transMgr);
        m_spec.initialize(m_agent);
    }

    /**
     * <p>reinitializeThresholding</p>
     */
    public void reinitializeThresholding() {
        if(m_thresholdVisitor!=null) {
            log().debug("reinitializeThresholding on "+this);
            m_thresholdVisitor.reload();
        }
    }
    
    /**
     * <p>getReadyRunnable</p>
     *
     * @return a {@link org.opennms.netmgt.scheduler.ReadyRunnable} object.
     */
    public ReadyRunnable getReadyRunnable() {
	return this;
    }

}
