// 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.template;

import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.naming.ConfigurationException;

import org.apache.cloudstack.storage.resource.SecondaryStorageResource;
import org.apache.log4j.Logger;

import com.cloud.agent.api.storage.CreateEntityDownloadURLAnswer;
import com.cloud.agent.api.storage.CreateEntityDownloadURLCommand;
import com.cloud.agent.api.storage.DeleteEntityDownloadURLAnswer;
import com.cloud.agent.api.storage.DeleteEntityDownloadURLCommand;
import com.cloud.agent.api.storage.UploadAnswer;
import com.cloud.agent.api.storage.UploadCommand;
import com.cloud.agent.api.storage.UploadProgressCommand;
import com.cloud.storage.Storage.ImageFormat;
import com.cloud.storage.StorageLayer;
import com.cloud.storage.Upload;
import com.cloud.storage.UploadVO;
import com.cloud.storage.template.FtpTemplateUploader;
import com.cloud.storage.template.Processor;
import com.cloud.storage.template.TemplateUploader;
import com.cloud.storage.template.TemplateUploader.Status;
import com.cloud.storage.template.TemplateUploader.UploadCompleteCallback;
import com.cloud.utils.NumbersUtil;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.script.Script;

public class UploadManagerImpl extends ManagerBase implements UploadManager {


    public class Completion implements UploadCompleteCallback {
        private final String jobId;

        public Completion(String jobId) {
            this.jobId = jobId;
        }

        @Override
        public void uploadComplete(Status status) {
            setUploadStatus(jobId, status);
        }
    }

    private static class UploadJob {
        private final TemplateUploader tu;
        private final String jobId;
        private final String name;
        private final ImageFormat format;
        private String tmpltPath;
        private String description;
        private String checksum;
        private Long accountId;
        private String installPathPrefix;
        private long templatesize;
        private long id;

        public UploadJob(TemplateUploader tu, String jobId, long id, String name, ImageFormat format, boolean hvm, Long accountId, String descr, String cksum, String installPathPrefix) {
            super();
            this.tu = tu;
            this.jobId = jobId;
            this.name = name;
            this.format = format;
            this.accountId = accountId;
            this.description = descr;
            this.checksum = cksum;
            this.installPathPrefix = installPathPrefix;
            this.templatesize = 0;
            this.id = id;
        }

        public TemplateUploader getTd() {
            return tu;
        }

        public String getDescription() {
            return description;
        }

        public String getChecksum() {
            return checksum;
        }

        public UploadJob(TemplateUploader td, String jobId, UploadCommand cmd) {
            this.tu = td;
            this.jobId = jobId;
            this.name = cmd.getName();
            this.format = cmd.getFormat();           
        }

        public TemplateUploader getTemplateUploader() {
            return tu;
        }

        public String getJobId() {
            return jobId;
        }

        public String getTmpltName() {
            return name;
        }

        public ImageFormat getFormat() {
            return format;
        }

        public Long getAccountId() {
            return accountId;
        }

        public long getId() {
            return id;
        }

        public void setTmpltPath(String tmpltPath) {
            this.tmpltPath = tmpltPath;
        }

        public String getTmpltPath() {
            return tmpltPath;
        }

        public String getInstallPathPrefix() {
            return installPathPrefix;
        }

        public void cleanup() {
            if (tu != null) {
                String upldPath = tu.getUploadLocalPath();
                if (upldPath != null) {
                    File f = new File(upldPath);
                    f.delete();
                }
            }
        }

        public void setTemplatesize(long templatesize) {
            this.templatesize = templatesize;
        }

        public long getTemplatesize() {
            return templatesize;
        }
    }
    public static final Logger s_logger = Logger.getLogger(UploadManagerImpl.class);
    private ExecutorService threadPool;
    private final Map<String, UploadJob> jobs = new ConcurrentHashMap<String, UploadJob>();
    private String parentDir;
    private List<Processor> _processors;
    private String publicTemplateRepo;
    private final String extractMountPoint = "/mnt/SecStorage/extractmnt";
    private StorageLayer _storage;
    private int installTimeoutPerGig;
    private boolean _sslCopy;
    private boolean hvm;


    @Override
    public String uploadPublicTemplate(long id, String url, String name,
            ImageFormat format, Long accountId, String descr,
            String cksum, String installPathPrefix, String userName,
            String passwd, long templateSizeInBytes) {		

        UUID uuid = UUID.randomUUID();
        String jobId = uuid.toString();

        String completePath = parentDir + File.separator + installPathPrefix;
        s_logger.debug("Starting upload from " + completePath);

        URI uri;
        try {
            uri = new URI(url);
        } catch (URISyntaxException e) {
            s_logger.error("URI is incorrect: " + url);
            throw new CloudRuntimeException("URI is incorrect: " + url);
        }
        TemplateUploader tu;
        if ((uri != null) && (uri.getScheme() != null)) {
            if (uri.getScheme().equalsIgnoreCase("ftp")) {
                tu = new FtpTemplateUploader(completePath, url, new Completion(jobId), templateSizeInBytes);                                
            } else {
                s_logger.error("Scheme is not supported " + url);
                throw new CloudRuntimeException("Scheme is not supported " + url);
            }
        } else {
            s_logger.error("Unable to download from URL: " + url);
            throw new CloudRuntimeException("Unable to download from URL: " + url);
        }
        UploadJob uj = new UploadJob(tu, jobId, id, name, format, hvm, accountId, descr, cksum, installPathPrefix);
        jobs.put(jobId, uj);
        threadPool.execute(tu);

        return jobId;

    }

    @Override
    public String getUploadError(String jobId) {
        UploadJob uj = jobs.get(jobId);
        if (uj != null) {
            return uj.getTemplateUploader().getUploadError();
        }
        return null;
    }

    @Override
    public int getUploadPct(String jobId) {
        UploadJob uj = jobs.get(jobId);
        if (uj != null) {
            return uj.getTemplateUploader().getUploadPercent();
        }
        return 0;
    }

    @Override
    public Status getUploadStatus(String jobId) {
        UploadJob job = jobs.get(jobId);
        if (job != null) {
            TemplateUploader tu = job.getTemplateUploader();
            if (tu != null) {
                return tu.getStatus();
            }
        }
        return Status.UNKNOWN;
    }

    public static UploadVO.Status convertStatus(Status tds) {
        switch (tds) {
        case ABORTED:
            return UploadVO.Status.NOT_UPLOADED;
        case UPLOAD_FINISHED:
            return UploadVO.Status.UPLOAD_IN_PROGRESS;
        case IN_PROGRESS:
            return UploadVO.Status.UPLOAD_IN_PROGRESS;
        case NOT_STARTED:
            return UploadVO.Status.NOT_UPLOADED;
        case RECOVERABLE_ERROR:
            return UploadVO.Status.NOT_UPLOADED;
        case UNKNOWN:
            return UploadVO.Status.UNKNOWN;
        case UNRECOVERABLE_ERROR:
            return UploadVO.Status.UPLOAD_ERROR;
        case POST_UPLOAD_FINISHED:
            return UploadVO.Status.UPLOADED;
        default:
            return UploadVO.Status.UNKNOWN;
        }
    }

    @Override
    public com.cloud.storage.UploadVO.Status getUploadStatus2(String jobId) {
        return convertStatus(getUploadStatus(jobId));
    }
    @Override
    public String getPublicTemplateRepo() {
        // TODO Auto-generated method stub
        return null;
    }

    private UploadAnswer handleUploadProgressCmd(UploadProgressCommand cmd) {
        String jobId = cmd.getJobId();
        UploadAnswer answer;
        UploadJob uj = null;
        if (jobId != null)
            uj = jobs.get(jobId);
        if (uj == null) {           
            return new UploadAnswer(null, 0, "Cannot find job", com.cloud.storage.UploadVO.Status.UNKNOWN, "", "", 0);            
        }
        TemplateUploader td = uj.getTemplateUploader();
        switch (cmd.getRequest()) {
        case GET_STATUS:
            break;
        case ABORT:
            td.stopUpload();
            sleep();
            break;
            /*case RESTART:
            td.stopUpload();
            sleep();
            threadPool.execute(td);
            break;*/
        case PURGE:
            td.stopUpload();
            answer = new UploadAnswer(jobId, getUploadPct(jobId), getUploadError(jobId), getUploadStatus2(jobId), getUploadLocalPath(jobId), getInstallPath(jobId), getUploadTemplateSize(jobId));
            jobs.remove(jobId);
            return answer;
        default:
            break; // TODO
        }
        return new UploadAnswer(jobId, getUploadPct(jobId), getUploadError(jobId), getUploadStatus2(jobId), getUploadLocalPath(jobId), getInstallPath(jobId),
                getUploadTemplateSize(jobId));
    }	

    @Override
    public UploadAnswer handleUploadCommand(SecondaryStorageResource resource, UploadCommand cmd) {
        s_logger.warn("Handling the upload " +cmd.getInstallPath() + " " + cmd.getId());
        if (cmd instanceof UploadProgressCommand) {
            return handleUploadProgressCmd((UploadProgressCommand) cmd);
        }

        String user = null;
        String password = null;
        String jobId = uploadPublicTemplate(cmd.getId(), cmd.getUrl(), cmd.getName(), 
                cmd.getFormat(), cmd.getAccountId(), cmd.getDescription(),
                cmd.getChecksum(), cmd.getInstallPath(), user, password,
                cmd.getTemplateSizeInBytes());
        sleep();
        if (jobId == null) {
            return new UploadAnswer(null, 0, "Internal Error", com.cloud.storage.UploadVO.Status.UPLOAD_ERROR, "", "", 0);
        }
        return new UploadAnswer(jobId, getUploadPct(jobId), getUploadError(jobId), getUploadStatus2(jobId), getUploadLocalPath(jobId), getInstallPath(jobId),
                getUploadTemplateSize(jobId));
    }

    @Override
    public CreateEntityDownloadURLAnswer handleCreateEntityURLCommand(CreateEntityDownloadURLCommand cmd){       

        boolean isApacheUp = checkAndStartApache();
        if (!isApacheUp){
            String errorString = "Error in starting Apache server ";
            s_logger.error(errorString);
            return new CreateEntityDownloadURLAnswer(errorString, CreateEntityDownloadURLAnswer.RESULT_FAILURE);
        }
        // Create the directory structure so that its visible under apache server root
        String extractDir = "/var/www/html/userdata/";
        Script command = new Script("mkdir", s_logger);
        command.add("-p");
        command.add(extractDir);
        String result = command.execute();
        if (result != null) {
            String errorString = "Error in creating directory =" + result;
            s_logger.error(errorString);
            return new CreateEntityDownloadURLAnswer(errorString, CreateEntityDownloadURLAnswer.RESULT_FAILURE);
        }

        // Create a random file under the directory for security reasons.
        String uuid = cmd.getExtractLinkUUID();
        command = new Script("touch", s_logger);
        command.add(extractDir + uuid);
        result = command.execute();
        if (result != null) {
            String errorString = "Error in creating file " +uuid+ " ,error: " + result; 
            s_logger.warn(errorString);
            return new CreateEntityDownloadURLAnswer(errorString, CreateEntityDownloadURLAnswer.RESULT_FAILURE);
        }


        // Create a symbolic link from the actual directory to the template location. The entity would be directly visible under /var/www/html/userdata/cmd.getInstallPath();
        command = new Script("/bin/bash", s_logger);
        command.add("-c");
        command.add("ln -sf /mnt/SecStorage/" + cmd.getParent() + File.separator + cmd.getInstallPath() + " " + extractDir + uuid);
        result = command.execute();
        if (result != null) {
            String errorString = "Error in linking  err=" + result; 
            s_logger.error(errorString);
            return new CreateEntityDownloadURLAnswer(errorString, CreateEntityDownloadURLAnswer.RESULT_FAILURE);
        }

        return new CreateEntityDownloadURLAnswer("", CreateEntityDownloadURLAnswer.RESULT_SUCCESS);

    }

    @Override
    public DeleteEntityDownloadURLAnswer handleDeleteEntityDownloadURLCommand(DeleteEntityDownloadURLCommand cmd){

        //Delete the soft link. Example path = volumes/8/74eeb2c6-8ab1-4357-841f-2e9d06d1f360.vhd
        s_logger.warn("handleDeleteEntityDownloadURLCommand Path:"+cmd.getPath() + " Type:" +cmd.getType().toString());
        String path = cmd.getPath();
        Script command = new Script("/bin/bash", s_logger);
        command.add("-c");

        //We just need to remove the UUID.vhd
        String extractUrl = cmd.getExtractUrl();
        command.add("unlink /var/www/html/userdata/" +extractUrl.substring(extractUrl.lastIndexOf(File.separator) + 1));
        String result = command.execute();
        if (result != null) {
            String errorString = "Error in deleting =" + result;
            s_logger.warn(errorString);
            return new DeleteEntityDownloadURLAnswer(errorString, CreateEntityDownloadURLAnswer.RESULT_FAILURE);
        }

        // If its a volume also delete the Hard link since it was created only for the purpose of download.
        if(cmd.getType() == Upload.Type.VOLUME){
            command = new Script("/bin/bash", s_logger);
            command.add("-c");
            command.add("rm -f /mnt/SecStorage/" + cmd.getParentPath() +File.separator+ path);
            s_logger.warn(" " +parentDir +File.separator+ path);
            result = command.execute();
            if (result != null) {
                String errorString = "Error in linking  err=" + result;
                s_logger.warn(errorString);
                return new DeleteEntityDownloadURLAnswer(errorString, CreateEntityDownloadURLAnswer.RESULT_FAILURE);
            }
        }

        return new DeleteEntityDownloadURLAnswer("", CreateEntityDownloadURLAnswer.RESULT_SUCCESS);
    }

    private String getInstallPath(String jobId) {
        // TODO Auto-generated method stub
        return null;
    }

    private String getUploadLocalPath(String jobId) {
        // TODO Auto-generated method stub
        return null;
    }

    private long getUploadTemplateSize(String jobId){
        UploadJob uj = jobs.get(jobId);
        if (uj != null) {
            return uj.getTemplatesize();
        }
        return 0;
    }

    @Override
    public boolean configure(String name, Map<String, Object> params)
            throws ConfigurationException {

        String value = null;

        _storage = (StorageLayer) params.get(StorageLayer.InstanceConfigKey);
        if (_storage == null) {
            value = (String) params.get(StorageLayer.ClassConfigKey);
            if (value == null) {
                throw new ConfigurationException("Unable to find the storage layer");
            }

            Class<StorageLayer> clazz;
            try {
                clazz = (Class<StorageLayer>) Class.forName(value);
                _storage = clazz.newInstance();
            } catch (ClassNotFoundException e) {
                throw new ConfigurationException("Unable to instantiate " + value);
            } catch (InstantiationException e) {
                throw new ConfigurationException("Unable to instantiate " + value);
            } catch (IllegalAccessException e) {
                throw new ConfigurationException("Unable to instantiate " + value);
            }
        }
        String useSsl = (String)params.get("sslcopy");
        if (useSsl != null) {
            _sslCopy = Boolean.parseBoolean(useSsl);

        }
        String inSystemVM = (String)params.get("secondary.storage.vm");
        if (inSystemVM != null && "true".equalsIgnoreCase(inSystemVM)) {
            s_logger.info("UploadManager: starting additional services since we are inside system vm");
            startAdditionalServices();
            //blockOutgoingOnPrivate();
        }

        value = (String) params.get("install.timeout.pergig");
        this.installTimeoutPerGig = NumbersUtil.parseInt(value, 15 * 60) * 1000;

        value = (String) params.get("install.numthreads");
        final int numInstallThreads = NumbersUtil.parseInt(value, 10);        

        String scriptsDir = (String) params.get("template.scripts.dir");
        if (scriptsDir == null) {
            scriptsDir = "scripts/storage/secondary";
        }

        // Add more processors here.
        threadPool = Executors.newFixedThreadPool(numInstallThreads);

        return true;
    }

    private void startAdditionalServices() {


        Script command = new Script("rm", s_logger);
        command.add("-rf");
        command.add(extractMountPoint);
        String result = command.execute();
        if (result != null) {
            s_logger.warn("Error in creating file " +extractMountPoint+ " ,error: " + result );
            return;
        }

        command = new Script("touch", s_logger);
        command.add(extractMountPoint);
        result = command.execute();
        if (result != null) {
            s_logger.warn("Error in creating file " +extractMountPoint+ " ,error: " + result );
            return;
        }

        command = new Script("/bin/bash", s_logger);
        command.add("-c");
        command.add("ln -sf " + parentDir + " " +extractMountPoint);
        result = command.execute();
        if (result != null) {
            s_logger.warn("Error in linking  err=" + result );
            return;
        }

    }

    /**
     * Get notified of change of job status. Executed in context of uploader thread
     * 
     * @param jobId
     *            the id of the job
     * @param status
     *            the status of the job
     */
    public void setUploadStatus(String jobId, Status status) {
        UploadJob uj = jobs.get(jobId);
        if (uj == null) {
            s_logger.warn("setUploadStatus for jobId: " + jobId + ", status=" + status + " no job found");
            return;
        }
        TemplateUploader tu = uj.getTemplateUploader();
        s_logger.warn("Upload Completion for jobId: " + jobId + ", status=" + status);
        s_logger.warn("UploadedBytes=" + tu.getUploadedBytes() + ", error=" + tu.getUploadError() + ", pct=" + tu.getUploadPercent());

        switch (status) {
        case ABORTED:
        case NOT_STARTED:
        case UNRECOVERABLE_ERROR:
            // Delete the entity only if its a volume. TO DO - find a better way of finding it a volume.
            if(uj.getTemplateUploader().getUploadLocalPath().indexOf("volume") > -1){
                uj.cleanup();
            }
            break;
        case UNKNOWN:
            return;
        case IN_PROGRESS:
            s_logger.info("Resuming jobId: " + jobId + ", status=" + status);
            tu.setResume(true);
            threadPool.execute(tu);
            break;
        case RECOVERABLE_ERROR:
            threadPool.execute(tu);
            break;
        case UPLOAD_FINISHED:
            tu.setUploadError("Upload success, starting install ");
            String result = postUpload(jobId);
            if (result != null) {
                s_logger.error("Failed post upload script: " + result);
                tu.setStatus(Status.UNRECOVERABLE_ERROR);
                tu.setUploadError("Failed post upload script: " + result);
            } else {
                s_logger.warn("Upload completed successfully at " + new SimpleDateFormat().format(new Date()));
                tu.setStatus(Status.POST_UPLOAD_FINISHED);
                tu.setUploadError("Upload completed successfully at " + new SimpleDateFormat().format(new Date()));
            }
            // Delete the entity only if its a volume. TO DO - find a better way of finding it a volume.
            if(uj.getTemplateUploader().getUploadLocalPath().indexOf("volume") > -1){
                uj.cleanup();
            }
            break;
        default:
            break;
        }
    }

    private String postUpload(String jobId) {
        return null;
    }

    private void sleep() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            // ignore
        }
    }

    private boolean checkAndStartApache() {

        //Check whether the Apache server is running
        Script command = new Script("/bin/bash", s_logger);
        command.add("-c");
        command.add("if [ -d /etc/apache2 ] ; then service apache2 status | grep pid; else service httpd status | grep pid; fi ");
        String result = command.execute();

        //Apache Server is not running. Try to start it.
        if (result != null) { 				  	

            /*s_logger.warn("Apache server not running, trying to start it");
			String port = Integer.toString(TemplateConstants.DEFAULT_TMPLT_COPY_PORT);
			String intf = TemplateConstants.DEFAULT_TMPLT_COPY_INTF;

			command = new Script("/bin/bash", s_logger);
			command.add("-c");
			command.add("iptables -D INPUT -i " + intf + " -p tcp -m state --state NEW -m tcp --dport " + port + " -j DROP;" +
				        "iptables -D INPUT -i " + intf + " -p tcp -m state --state NEW -m tcp --dport " + port + " -j HTTP;" +
				        "iptables -D INPUT -i " + intf + " -p tcp -m state --state NEW -m tcp --dport " + "443" + " -j DROP;" +
				        "iptables -D INPUT -i " + intf + " -p tcp -m state --state NEW -m tcp --dport " + "443" + " -j HTTP;" +
				        "iptables -F HTTP;" +
				        "iptables -X HTTP;" +
				        "iptables -N HTTP;" +
					    "iptables -I INPUT -i " + intf + " -p tcp -m state --state NEW -m tcp --dport " + port + " -j DROP;" +
					    "iptables -I INPUT -i " + intf + " -p tcp -m state --state NEW -m tcp --dport " + "443" + " -j DROP;" +
					    "iptables -I INPUT -i " + intf + " -p tcp -m state --state NEW -m tcp --dport " + port + " -j HTTP;" +
			            "iptables -I INPUT -i " + intf + " -p tcp -m state --state NEW -m tcp --dport " + "443" + " -j HTTP;");

			result = command.execute();
			if (result != null) {
				s_logger.warn("Error in opening up httpd port err=" + result );
				return false;
			}*/			

            command = new Script("/bin/bash", s_logger);
            command.add("-c");
            command.add("if [ -d /etc/apache2 ] ; then service apache2 start; else service httpd start; fi ");
            result = command.execute();
            if (result != null) {
                s_logger.warn("Error in starting httpd service err=" + result );
                return false;
            }
        }

        return true;
    }
}    
