// 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.hypervisor.kvm.storage;

import com.cloud.hypervisor.kvm.resource.LibvirtConnection;
import com.cloud.hypervisor.kvm.resource.LibvirtDomainXMLParser;
import com.cloud.hypervisor.kvm.resource.LibvirtVMDef;
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.libvirt.Connect;
import org.libvirt.Domain;
import org.libvirt.LibvirtException;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class IscsiStorageCleanupMonitor implements Runnable{
    protected Logger logger = LogManager.getLogger(getClass());
    private static final int CLEANUP_INTERVAL_SEC = 60; // check every X seconds
    private static final String ISCSI_PATH_PREFIX = "/dev/disk/by-path";
    private static final String KEYWORD_ISCSI = "iscsi";
    private static final String KEYWORD_IQN = "iqn";
    private static final String REGEX_PART = "\\S+part\\d+$";

    private IscsiAdmStorageAdaptor iscsiStorageAdaptor;

    private Map<String, Boolean> diskStatusMap;

    public IscsiStorageCleanupMonitor() {
        diskStatusMap = new HashMap<>();
        logger.debug("Initialize cleanup thread");
        iscsiStorageAdaptor = new IscsiAdmStorageAdaptor();
    }


    private class Monitor extends ManagedContextRunnable {

        @Override
        protected void runInContext() {
            Connect conn = null;
            try {
                conn = LibvirtConnection.getConnection();

                //populate all the iscsi disks currently attached to this host
                File[] iscsiVolumes = new File(ISCSI_PATH_PREFIX).listFiles();
                if (iscsiVolumes == null || iscsiVolumes.length == 0) {
                    logger.debug("No iscsi sessions found for cleanup");
                    return;
                }

                // set all status values to false
                initializeDiskStatusMap(iscsiVolumes);

                // check if iscsi sessions belong to any VM
                updateDiskStatusMapWithInactiveIscsiSessions(conn);

                // disconnect stale iscsi sessions
                disconnectInactiveSessions();

            } catch (LibvirtException e) {
                logger.warn("[ignored] Error trying to cleanup ", e);
            }
        }

        private boolean isIscsiDisk(String path) {
            return path.startsWith(ISCSI_PATH_PREFIX) && path.contains(KEYWORD_ISCSI) && path.contains(KEYWORD_IQN);
        }

        /**
         * for each volume if the volume is path is of type iscsi, add to diskstatusmap and set status to false.
         * @param iscsiVolumes
         */
        private void initializeDiskStatusMap(File[] iscsiVolumes){
            diskStatusMap.clear();
            for( File v : iscsiVolumes) {
                if (isIscsiDisk(v.getAbsolutePath())) {
                    logger.debug("found iscsi disk by cleanup thread, marking inactive: " + v.getAbsolutePath());
                    diskStatusMap.put(v.getAbsolutePath(), false);
                }
            }
        }

        /** Loop over list of VMs or domains, get disks, if disk is in diskStatusMap, set status value to true.
         *
         * @param conn
         */
        private void updateDiskStatusMapWithInactiveIscsiSessions(Connect conn){
            try {
                int[] domains = conn.listDomains();
                logger.debug(String.format("found %d domains", domains.length));
                for (int domId : domains) {
                    Domain dm = conn.domainLookupByID(domId);
                    final String domXml = dm.getXMLDesc(0);
                    final LibvirtDomainXMLParser parser = new LibvirtDomainXMLParser();
                    parser.parseDomainXML(domXml);
                    List<LibvirtVMDef.DiskDef> disks = parser.getDisks();

                    //check the volume map. If an entry exists change the status to True
                    for (final LibvirtVMDef.DiskDef disk : disks) {
                        if (diskStatusMap.containsKey(disk.getDiskPath())&&!disk.getDiskPath().matches(REGEX_PART)) {
                            diskStatusMap.put(disk.getDiskPath(), true);
                            logger.debug("active disk found by cleanup thread" + disk.getDiskPath());
                        }
                    }
                }
            } catch (LibvirtException e) {
                logger.warn("[ignored] Error trying to cleanup ", e);
            }

        }

        /**
         * When the state is false, the iscsi sessions are stale. They may be
         * removed. We go through each volume which is false, check iscsiadm,
         * if the volume still exisits, logout of that volume and remove it from the map

         * XXX: It is possible that someone had manually added an iSCSI volume.
         * we would not be able to detect that
         */
        private void disconnectInactiveSessions(){

            for (String diskPath : diskStatusMap.keySet()) {
                if (!diskStatusMap.get(diskPath)) {
                    if (Files.exists(Paths.get(diskPath))) {
                        try {
                            logger.info("Cleaning up disk " + diskPath);
                            iscsiStorageAdaptor.disconnectPhysicalDiskByPath(diskPath);
                        } catch (Exception e) {
                            logger.warn("[ignored] Error cleaning up " + diskPath, e);
                        }
                    }
                }
            }

        }
    }

    @Override
    public void run() {
        while(true) {
            try {
                Thread.sleep(CLEANUP_INTERVAL_SEC * 1000);
            } catch (InterruptedException e) {
                logger.debug("[ignored] interrupted between heartbeats.");
            }

            Thread monitorThread = new Thread(new Monitor());
            monitorThread.start();
            try {
                monitorThread.join();
            } catch (InterruptedException e) {
                logger.debug("[ignored] interrupted joining monitor.");
            }
        }
    }
}
