//
// 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 org.apache.cloudstack.storage;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import javax.annotation.PreDestroy;

import com.cloud.storage.StorageLayer;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.script.OutputInterpreter;
import com.cloud.utils.script.Script;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.utils.identity.ManagementServerNode;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.springframework.stereotype.Component;

@Component
public class NfsMountManagerImpl implements NfsMountManager {
    protected Logger logger = LogManager.getLogger(getClass());

    private StorageLayer storage;
    private int timeout;
    private final Random rand = new Random(System.currentTimeMillis());
    private final ConcurrentMap<String, String> storageMounts = new ConcurrentHashMap<>();

    public static final ConfigKey<String> MOUNT_PARENT = new ConfigKey<>("Advanced", String.class,
            "mount.parent", "/var/cloudstack/mnt",
            "The mount point on the Management Server for Secondary Storage.",
            true, ConfigKey.Scope.Global);

    public NfsMountManagerImpl(StorageLayer storage, int timeout) {
        this.storage = storage;
        this.timeout = timeout;
    }

    public String getMountPoint(String storageUrl, String nfsVersion) {
        String mountPoint = storageMounts.get(storageUrl);
        if (mountPoint != null) {
            return mountPoint;
        }

        URI uri;
        try {
            uri = new URI(storageUrl);
        } catch (URISyntaxException e) {
            logger.error("Invalid storage URL format ", e);
            throw new CloudRuntimeException("Unable to create mount point due to invalid storage URL format " + storageUrl);
        }

        mountPoint = mount(uri.getHost() + ":" + uri.getPath(), MOUNT_PARENT.value(), nfsVersion);
        if (mountPoint == null) {
            logger.error("Unable to create mount point for " + storageUrl);
            throw new CloudRuntimeException("Unable to create mount point for " + storageUrl);
        }

        storageMounts.putIfAbsent(storageUrl, mountPoint);
        return mountPoint;
    }

    private String mount(String path, String parent, String nfsVersion) {
        String mountPoint = setupMountPoint(parent);
        if (mountPoint == null) {
            logger.warn("Unable to create a mount point");
            return null;
        }

        Script command = new Script(true, "mount", timeout, logger);
        command.add("-t", "nfs");
        if (nfsVersion != null){
            command.add("-o", "vers=" + nfsVersion);
        }
        // command.add("-o", "soft,timeo=133,retrans=2147483647,tcp,acdirmax=0,acdirmin=0");
        if ("Mac OS X".equalsIgnoreCase(System.getProperty("os.name"))) {
            command.add("-o", "resvport");
        }
        command.add(path);
        command.add(mountPoint);
        String result = command.execute();
        if (result != null) {
            logger.warn("Unable to mount " + path + " due to " + result);
            deleteMountPath(mountPoint);
            return null;
        }

        // Change permissions for the mountpoint
        Script script = new Script(true, "chmod", timeout, logger);
        script.add("1777", mountPoint);
        result = script.execute();
        if (result != null) {
            logger.warn("Unable to set permissions for " + mountPoint + " due to " + result);
        }
        return mountPoint;
    }

    private String setupMountPoint(String parent) {
        String mountPoint = null;
        for (int i = 0; i < 10; i++) {
            String mntPt = parent + File.separator + String.valueOf(ManagementServerNode.getManagementServerId()) + "." + Integer.toHexString(rand.nextInt(Integer.MAX_VALUE));
            File file = new File(mntPt);
            if (!file.exists()) {
                if (storage.mkdir(mntPt)) {
                    mountPoint = mntPt;
                    break;
                }
            }
            logger.error("Unable to create mount: " + mntPt);
        }

        return mountPoint;
    }

    private void umount(String localRootPath) {
        if (!mountExists(localRootPath)) {
            return;
        }
        Script command = new Script(true, "umount", timeout, logger);
        command.add(localRootPath);
        String result = command.execute();
        if (result != null) {
            // Fedora Core 12 errors out with any -o option executed from java
            String errMsg = "Unable to umount " + localRootPath + " due to " + result;
            logger.error(errMsg);
            throw new CloudRuntimeException(errMsg);
        }
        deleteMountPath(localRootPath);
        logger.debug("Successfully umounted " + localRootPath);
    }

    private void deleteMountPath(String localRootPath) {
        try {
            Files.deleteIfExists(Paths.get(localRootPath));
        } catch (IOException e) {
            logger.warn(String.format("unable to delete mount directory %s:%s.%n", localRootPath, e.getMessage()));
        }
    }

    private boolean mountExists(String localRootPath) {
        Script script = new Script(true, "mount", timeout, logger);
        PathParser parser = new PathParser(localRootPath);
        script.execute(parser);
        return parser.getPaths().stream().filter(s -> s.contains(localRootPath)).findAny().map(s -> true).orElse(false);
    }

    public static class PathParser extends OutputInterpreter {
        String _parent;
        List<String> paths = new ArrayList<>();

        public PathParser(String parent) {
            _parent = parent;
        }

        @Override
        public String interpret(BufferedReader reader) throws IOException {
            String line;
            while ((line = reader.readLine()) != null) {
                paths.add(line);
            }
            return null;
        }

        public List<String> getPaths() {
            return paths;
        }

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

    @PreDestroy
    public void destroy() {
        logger.info("Clean up mounted NFS mount points used in current session.");
        storageMounts.values().stream().forEach(this::umount);
    }
}
