/*
 * Licensed to The OpenNMS Group, Inc (TOG) under one or more
 * contributor license agreements.  See the LICENSE.md file
 * distributed with this work for additional information
 * regarding copyright ownership.
 *
 * TOG licenses this file to You under the GNU Affero General
 * Public License Version 3 (the "License") or (at your option)
 * any later version.  You may not use this file except in
 * compliance with the License.  You may obtain a copy of the
 * License at:
 *
 *      https://www.gnu.org/licenses/agpl-3.0.txt
 *
 * 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.opennms.upgrade.implementations;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.io.FileUtils;
import org.opennms.core.utils.ConfigFileConstants;
import org.opennms.core.utils.DBUtils;
import org.opennms.netmgt.config.DataCollectionConfigFactory;
import org.opennms.netmgt.config.DefaultDataCollectionConfigDao;
import org.opennms.netmgt.rrd.util.RrdConvertUtils;
import org.opennms.netmgt.rrd.model.v1.RRDv1;
import org.opennms.netmgt.rrd.model.v3.RRDv3;
import org.opennms.upgrade.api.AbstractOnmsUpgrade;
import org.opennms.upgrade.api.OnmsUpgradeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.FileSystemResource;

/**
 * The Class RRD/JRB Migrator for SNMP Interfaces Data (Online Version)
 * 
 * <p>1.12 always add the MAC Address to the snmpinterface table if exist, which
 * is different from the 1.10 behavior. For this reason, some interfaces are going
 * to appear twice, and the data must be merged.</p>
 * 
 * <p>This tool requires that OpenNMS 1.12 is running for a while to be sure that
 * all the MAC addresses have been updated, and the directories already exist.</p>
 * 
 * <p>Issues fixed:</p>
 * <ul>
 * <li>NMS-6056</li>
 * </ul>
 * 
 * @author <a href="mailto:agalue@opennms.org">Alejandro Galue</a> 
 */
public class SnmpInterfaceRrdMigratorOnline extends AbstractOnmsUpgrade {

    private static final Logger LOG = LoggerFactory.getLogger(SnmpInterfaceRrdMigratorOnline.class);

    /** The interfaces to merge. */
    private List<SnmpInterfaceUpgrade> interfacesToMerge;

    /**
     * Instantiates a new SNMP interface RRD migrator online.
     *
     * @throws OnmsUpgradeException the OpenNMS upgrade exception
     */
    public SnmpInterfaceRrdMigratorOnline() throws OnmsUpgradeException {
        super();
    }

    /* (non-Javadoc)
     * @see org.opennms.upgrade.api.OnmsUpgrade#getOrder()
     */
    public int getOrder() {
        return 2;
    }

    /* (non-Javadoc)
     * @see org.opennms.upgrade.api.OnmsUpgrade#getDescription()
     */
    public String getDescription() {
        return "Merge SNMP Interface directories (Online Version): NMS-6056";
    }

    /* (non-Javadoc)
     * @see org.opennms.upgrade.api.OnmsUpgrade#requiresOnmsRunning()
     */
    public boolean requiresOnmsRunning() {
        return true;
    }

    /* (non-Javadoc)
     * @see org.opennms.upgrade.api.OnmsUpgrade#preExecute()
     */
    public void preExecute() throws OnmsUpgradeException {
        printMainSettings();
        if (getRrdExtension() == null) {
            throw new OnmsUpgradeException("Can't find the configured extension for JRB/RRD.");
        }
        try {
            // Manually initialization of the DataCollectionConfigDao to avoid bootstrap Spring Framework and create a new connection pool.
            File cfgFile = ConfigFileConstants.getFile(ConfigFileConstants.DATA_COLLECTION_CONF_FILE_NAME);
            DefaultDataCollectionConfigDao config = new DefaultDataCollectionConfigDao();
            config.setConfigResource(new FileSystemResource(cfgFile));
            config.afterPropertiesSet();
            config.getConfiguredResourceTypes();
            DataCollectionConfigFactory.setInstance(config);
        } catch (Exception e) {
            throw new OnmsUpgradeException("Can't initialize datacollection-config.xml because " + e.getMessage());
        }
        interfacesToMerge = getInterfacesToMerge();
    }

    /* (non-Javadoc)
     * @see org.opennms.upgrade.api.OnmsUpgrade#postExecute()
     */
    public void postExecute() throws OnmsUpgradeException {
        // The idea is to remove the backups only if the current interface was updated. A global post-execute is not necessary.
    }

    /* (non-Javadoc)
     * @see org.opennms.upgrade.api.OnmsUpgrade#rollback()
     */
    @Override
    public void rollback() throws OnmsUpgradeException {
        // The idea is to roll back only if the current interface was not updated. A global roll back is not necessary.
    }

    /* (non-Javadoc)
     * @see org.opennms.upgrade.api.OnmsUpgrade#execute()
     */
    @Override
    public void execute() throws OnmsUpgradeException {
        for (SnmpInterfaceUpgrade intf : interfacesToMerge) {
            try {
                createBackup(intf);
                merge(intf.getOldInterfaceDir(), intf.getNewInterfaceDir());
                removeBackup(intf);
            } catch (Exception e) {
                StringWriter w = new StringWriter();
                PrintWriter p = new PrintWriter(w);
                e.printStackTrace(p);
                log("Error: Can't upgrade %s because %s: %s. Rolling back changes\n", intf, e.getMessage(), w.toString());
                restoreBackup(intf);
            }
        }
    }

    /**
     * Crates a backup of the SNMP interface files.
     *
     * @param intf the SNMP interface
     * @throws OnmsUpgradeException the OpenNMS upgrade exception
     */
    private void createBackup(SnmpInterfaceUpgrade intf) throws OnmsUpgradeException {
        File[] targets = { intf.getOldInterfaceDir(), intf.getNewInterfaceDir() };
        for (File target : targets) {
            if (target.exists()) {
                log("Backing up: %s\n", target);
                zipDir(new File(target.getAbsolutePath() + ZIP_EXT), target);
            }
        }
    }

    /**
     * Restore the backup for a specific SNMP interface
     *
     * @param intf the SNMP interface
     * @throws OnmsUpgradeException the OpenNMS upgrade exception
     */
    private void restoreBackup(SnmpInterfaceUpgrade intf) throws OnmsUpgradeException {
        File[] targets = { intf.getOldInterfaceDir(), intf.getNewInterfaceDir() };
        for (File target : targets) {
            File zip = new File(target.getAbsolutePath() + ZIP_EXT);
            try {
                FileUtils.deleteDirectory(target);
            } catch (IOException e1) {
                log("Warning: can't delete directory %s\n", target);
            }
            if (!target.mkdirs()) {
                LOG.warn("Could not make directory: {}", target.getPath());
            }
            if (zip.exists()) {
                unzipFile(zip, target);
                if (!zip.delete()) {
                    LOG.warn("Could not delete file: {}", zip.getPath());
                }
            }
        }
    }

    /**
     * Removes the backups for a specific SNMP interface
     *
     * @param intf the SNMP interface
     * @throws OnmsUpgradeException the OpenNMS upgrade exception
     */
    private void removeBackup(SnmpInterfaceUpgrade intf) {
        File[] targets = { intf.getOldInterfaceDir(), intf.getNewInterfaceDir() };
        for (File target : targets) {
            File zip = new File(target.getAbsolutePath() + ZIP_EXT);
            if (zip.exists()) {
                log("Removing backup: %s\n", zip);
                if (!zip.delete()) {
                    LOG.warn("Could not delete file: {}", zip.getPath());
                }
            }
        }
    }

    /**
     * Gets the interfaces to merge.
     *
     * @return the list of interfaces to merge
     * @throws OnmsUpgradeException the OpenNMS upgrade exception
     */
    protected List<SnmpInterfaceUpgrade> getInterfacesToMerge() throws OnmsUpgradeException {
        List<SnmpInterfaceUpgrade> interfacesToMerge = new ArrayList<>();
        Connection conn = getDbConnection();
        final DBUtils db = new DBUtils(getClass());
        db.watch(conn);
        try {
            Statement st = conn.createStatement();
            db.watch(st);
            String query = "SELECT n.nodeid, n.foreignsource, n.foreignid, i.snmpifdescr, i.snmpifname, i.snmpphysaddr from node n, snmpinterface i where n.nodeid = i.nodeid and i.snmpphysaddr is not null order by n.nodeid, i.snmpifdescr";
            ResultSet rs = st.executeQuery(query);
            db.watch(rs);
            int count = 0;
            while (rs.next()) {
                count++;
                SnmpInterfaceUpgrade intf = new SnmpInterfaceUpgrade(rs, isStoreByForeignSourceEnabled());
                if (intf.shouldMerge()) {
                    interfacesToMerge.add(intf);
                }
                log("  Should merge %s ? %s\n", intf, intf.shouldMerge());
            }
            log("  Found %s of %s SNMP interfaces that require a merge.\n", interfacesToMerge.size(), count);
        } catch (Throwable t) {
            log("Error: can't retrieve the required data from the OpenNMS Database or there were problems while processing them.\n");
            String reason = t.getMessage();
            if (reason == null) {
                reason = "Unknown";
            }
            log("Reason(%s): %s\n", t.getClass().getName(), reason);
            t.printStackTrace(); // TODO This is not elegant, but it helps.
        } finally {
            db.cleanUp();
        }
        return interfacesToMerge;
    }

    /**
     * Merge.
     *
     * @param oldDir the old directory
     * @param newDir the new directory
     * @throws Exception the exception
     */
    protected void merge(File oldDir, File newDir) throws Exception {
        log("Merging data from %s to %s\n", oldDir, newDir);
        if (newDir.exists()) {
            File[] rrdFiles = getFiles(oldDir, getRrdExtension());
            if (rrdFiles == null) {
                log("Warning: there are no %s files on %s\n", getRrdExtension(), oldDir);
            } else {
                for (File source : rrdFiles) {
                    File dest = new File(newDir, source.getName());
                    if (dest.exists()) {
                        if (isRrdToolEnabled()) {
                            mergeRrd(source, dest);
                        } else {
                            mergeJrb(source, dest);
                        }
                    } else {
                        log("  Warning: %s doesn't exist\n", dest);
                    }
                }
            }
            try {
                log("  removing old directory %s\n", oldDir.getName());
                FileUtils.deleteDirectory(oldDir);
            } catch (Exception e) {
                log("  Warning: can't delete old directory because %s", e.getMessage());
            }
        } else {
            try {
                log("  moving %s to %s\n", oldDir.getName(), newDir.getName());
                FileUtils.moveDirectory(oldDir, newDir);
            } catch (Exception e) {
                log("  Warning: can't rename directory because %s", e.getMessage());
            }
        }
    }

    /**
     * Merge RRDs.
     *
     * @param source the source RRD
     * @param dest the destination RRD
     * @throws Exception the exception
     */
    protected void mergeRrd(File source, File dest) throws Exception {
        log("  merging RRD %s into %s\n", source, dest);
        RRDv3 rrdSrc = RrdConvertUtils.dumpRrd(source);
        RRDv3 rrdDst = RrdConvertUtils.dumpRrd(dest);
        if (rrdSrc == null || rrdDst == null) {
            log("  Warning: can't load RRDs (ingoring merge).\n");
            return;
        }
        rrdDst.merge(rrdSrc);
        final File outputFile = new File(dest.getCanonicalPath() + ".merged");
        RrdConvertUtils.restoreRrd(rrdDst, outputFile);
        if (dest.exists()) {
            FileUtils.deleteQuietly(dest);
        }
        FileUtils.moveFile(outputFile, dest);
    }

    /**
     * Merge JRBs.
     *
     * @param source the source JRB
     * @param dest the destination JRB
     * @throws Exception the exception
     */
    protected void mergeJrb(File source, File dest) throws Exception {
        log("  merging JRB %s into %s\n", source, dest);
        RRDv1 rrdSrc = RrdConvertUtils.dumpJrb(source);
        RRDv1 rrdDst = RrdConvertUtils.dumpJrb(dest);
        if (rrdSrc == null || rrdDst == null) {
            log("  Warning: can't load JRBs (ingoring merge).\n");
            return;
        }
        rrdDst.merge(rrdSrc);
        final File outputFile = new File(dest.getCanonicalPath() + ".merged");
        RrdConvertUtils.restoreJrb(rrdDst, outputFile);
        if (dest.exists()) {
            FileUtils.deleteQuietly(dest);
        }
        FileUtils.moveFile(outputFile, dest);
    }

    /**
     * Gets the node directory.
     *
     * @param nodeId the node id
     * @param foreignSource the foreign source
     * @param foreignId the foreign id
     * @return the node directory
     */
    protected File getNodeDirectory(int nodeId, String foreignSource, String foreignId) {
        File dir = new File(DataCollectionConfigFactory.getInstance().getRrdPath(), String.valueOf(nodeId));
        if (Boolean.getBoolean("org.opennms.rrd.storeByForeignSource") && !(foreignSource == null) && !(foreignId == null)) {
            File fsDir = new File(DataCollectionConfigFactory.getInstance().getRrdPath(), "fs" + File.separatorChar + foreignSource);
            dir = new File(fsDir, foreignId);
        }
        return dir;
    }

}
