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

import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;

import javax.inject.Inject;

import org.apache.cloudstack.api.BaseCmd;

import org.apache.cloudstack.api.command.user.iso.ExtractIsoCmd;
import org.apache.cloudstack.api.command.user.template.ExtractTemplateCmd;
import org.apache.cloudstack.api.command.user.volume.ExtractVolumeCmd;
import org.apache.cloudstack.api.response.ExtractResponse;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint;
import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector;
import org.apache.cloudstack.framework.async.AsyncCompletionCallback;
import org.apache.cloudstack.framework.jobs.AsyncJobManager;
import org.apache.cloudstack.jobs.JobInfo;
import org.apache.cloudstack.managed.context.ManagedContextTimerTask;

import com.cloud.agent.Listener;
import com.cloud.agent.api.AgentControlAnswer;
import com.cloud.agent.api.AgentControlCommand;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.Command;
import com.cloud.agent.api.StartupCommand;
import com.cloud.agent.api.StartupStorageCommand;
import com.cloud.agent.api.storage.UploadAnswer;
import com.cloud.agent.api.storage.UploadCommand;
import com.cloud.agent.api.storage.UploadProgressCommand;
import com.cloud.agent.api.storage.UploadProgressCommand.RequestType;
import com.cloud.api.ApiDBUtils;
import com.cloud.api.ApiSerializerHelper;
import com.cloud.host.Host;
import com.cloud.storage.Storage;
import com.cloud.storage.Upload.Status;
import com.cloud.storage.Upload.Type;
import com.cloud.storage.UploadVO;
import com.cloud.storage.dao.UploadDao;
import com.cloud.storage.upload.UploadState.UploadEvent;
import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

public class UploadListener implements Listener {

    private static final class StatusTask extends ManagedContextTimerTask {
        private final UploadListener ul;
        private final RequestType reqType;

        public StatusTask(UploadListener ul, RequestType req) {
            reqType = req;
            this.ul = ul;
        }

        @Override
        protected void runInContext() {
            ul.sendCommand(reqType);

        }
    }

    private static final class TimeoutTask extends ManagedContextTimerTask {
        private final UploadListener ul;

        public TimeoutTask(UploadListener ul) {
            this.ul = ul;
        }

        @Override
        protected void runInContext() {
            ul.checkProgress();
        }
    }

    protected Logger logger = LogManager.getLogger(getClass());
    public static final int SMALL_DELAY = 100;
    public static final long STATUS_POLL_INTERVAL = 10000L;

    public static final String UPLOADED = Status.UPLOADED.toString();
    public static final String NOT_UPLOADED = Status.NOT_UPLOADED.toString();
    public static final String UPLOAD_ERROR = Status.UPLOAD_ERROR.toString();
    public static final String UPLOAD_IN_PROGRESS = Status.UPLOAD_IN_PROGRESS.toString();
    public static final String UPLOAD_ABANDONED = Status.ABANDONED.toString();
    public static final Map<String, String> responseNameMap;
    static {
        Map<String, String> tempMap = new HashMap<String, String>();
        tempMap.put(Type.ISO.toString(), BaseCmd.getResponseNameByClass(ExtractIsoCmd.class));
        tempMap.put(Type.TEMPLATE.toString(), BaseCmd.getResponseNameByClass(ExtractTemplateCmd.class));
        tempMap.put(Type.VOLUME.toString(), BaseCmd.getResponseNameByClass(ExtractVolumeCmd.class));
        tempMap.put("DEFAULT", "extractresponse");
        responseNameMap = Collections.unmodifiableMap(tempMap);
    }

    private DataStore sserver;

    private UploadDao uploadDao;

    private final UploadMonitorImpl uploadMonitor;

    private UploadState currState;

    private UploadCommand cmd;

    private Timer timer;

    private StatusTask statusTask;
    private TimeoutTask timeoutTask;
    private Date lastUpdated = new Date();
    private String jobId;
    private Long accountId;
    private String typeName;
    private Type type;
    private long asyncJobId;
    private long eventId;
    private AsyncJobManager asyncMgr;
    private ExtractResponse resultObj;
    @Inject
    EndPointSelector _epSelector;

    public AsyncJobManager getAsyncMgr() {
        return asyncMgr;
    }

    public void setAsyncMgr(AsyncJobManager asyncMgr) {
        this.asyncMgr = asyncMgr;
    }

    public long getAsyncJobId() {
        return asyncJobId;
    }

    public void setAsyncJobId(long asyncJobId) {
        this.asyncJobId = asyncJobId;
    }

    public long getEventId() {
        return eventId;
    }

    public void setEventId(long eventId) {
        this.eventId = eventId;
    }

    private final Map<String, UploadState> stateMap = new HashMap<String, UploadState>();
    private Long uploadId;

    public UploadListener(DataStore host, Timer timerInput, UploadDao uploadDao, UploadVO uploadObj, UploadMonitorImpl uploadMonitor, UploadCommand cmd, Long accountId,
            String typeName, Type type, long eventId, long asyncJobId, AsyncJobManager asyncMgr) {
        sserver = host;
        this.uploadDao = uploadDao;
        this.uploadMonitor = uploadMonitor;
        this.cmd = cmd;
        uploadId = uploadObj.getId();
        this.accountId = accountId;
        this.typeName = typeName;
        this.type = type;
        initStateMachine();
        currState = getState(Status.NOT_UPLOADED.toString());
        timer = timerInput;
        timeoutTask = new TimeoutTask(this);
        timer.schedule(timeoutTask, 3 * STATUS_POLL_INTERVAL);
        this.eventId = eventId;
        this.asyncJobId = asyncJobId;
        this.asyncMgr = asyncMgr;
        String extractId = null;
        if (type == Type.VOLUME) {
            extractId = ApiDBUtils.findVolumeById(uploadObj.getTypeId()).getUuid();
        } else {
            extractId = ApiDBUtils.findTemplateById(uploadObj.getTypeId()).getUuid();
        }
        resultObj =
            new ExtractResponse(extractId, typeName, ApiDBUtils.findAccountById(accountId).getUuid(), Status.NOT_UPLOADED.toString(), ApiDBUtils.findUploadById(uploadId)
                .getUuid());
        resultObj.setResponseName(responseNameMap.get(type.toString()));
        updateDatabase(Status.NOT_UPLOADED, cmd.getUrl(), "");
    }

    public UploadListener(UploadMonitorImpl monitor) {
        uploadMonitor = monitor;
    }

    public void checkProgress() {
        transition(UploadEvent.TIMEOUT_CHECK, null);
    }

    @Override
    public int getTimeout() {
        return -1;
    }

    @Override
    public boolean isRecurring() {
        return false;
    }

    public void setCommand(UploadCommand cmd) {
        this.cmd = cmd;
    }

    public void setJobId(String jobId) {
        this.jobId = jobId;
    }

    public String getJobId() {
        return jobId;
    }

    @Override
    public boolean processAnswers(long agentId, long seq, Answer[] answers) {
        boolean processed = false;
        if (answers != null & answers.length > 0) {
            if (answers[0] instanceof UploadAnswer) {
                final UploadAnswer answer = (UploadAnswer)answers[0];
                if (getJobId() == null) {
                    setJobId(answer.getJobId());
                } else if (!getJobId().equalsIgnoreCase(answer.getJobId())) {
                    return false;//TODO
                }
                transition(UploadEvent.UPLOAD_ANSWER, answer);
                processed = true;
            }
        }
        return processed;
    }

    @Override
    public boolean processCommands(long agentId, long seq, Command[] commands) {
        return false;
    }

    @Override
    public void processHostAdded(long hostId) {
    }

    @Override
    public void processConnect(Host agent, StartupCommand cmd, boolean forRebalance) {
        if (!(cmd instanceof StartupStorageCommand)) {
            return;
        }

        long agentId = agent.getId();

        StartupStorageCommand storage = (StartupStorageCommand)cmd;
        if (storage.getResourceType() == Storage.StorageResourceType.STORAGE_HOST || storage.getResourceType() == Storage.StorageResourceType.SECONDARY_STORAGE) {
            uploadMonitor.handleUploadSync(agentId);
        }
    }

    @Override
    public AgentControlAnswer processControlCommand(long agentId, AgentControlCommand cmd) {
        return null;
    }

    public void setUploadInactive(Status reason) {
        uploadMonitor.handleUploadEvent(accountId, typeName, type, uploadId, reason, eventId);
    }

    public void logUploadStart() {
        //uploadMonitor.logEvent(accountId, event, "Storage server " + sserver.getName() + " started upload of " +type.toString() + " " + typeName, EventVO.LEVEL_INFO, eventId);
    }

    public void cancelTimeoutTask() {
        if (timeoutTask != null)
            timeoutTask.cancel();
    }

    public void cancelStatusTask() {
        if (statusTask != null)
            statusTask.cancel();
    }

    @Override
    public boolean processDisconnect(long agentId, com.cloud.host.Status state) {
        setDisconnected();
        return true;
    }

    @Override
    public void processHostAboutToBeRemoved(long hostId) {
    }

    @Override
    public void processHostRemoved(long hostId, long clusterId) {
    }

    @Override
    public boolean processTimeout(long agentId, long seq) {
        return true;
    }

    private void initStateMachine() {
        stateMap.put(Status.NOT_UPLOADED.toString(), new NotUploadedState(this));
        stateMap.put(Status.UPLOADED.toString(), new UploadCompleteState(this));
        stateMap.put(Status.UPLOAD_ERROR.toString(), new UploadErrorState(this));
        stateMap.put(Status.UPLOAD_IN_PROGRESS.toString(), new UploadInProgressState(this));
        stateMap.put(Status.ABANDONED.toString(), new UploadAbandonedState(this));
    }

    private UploadState getState(String stateName) {
        return stateMap.get(stateName);
    }

    private synchronized void transition(UploadEvent event, Object evtObj) {
        if (currState == null) {
            return;
        }
        String prevName = currState.getName();
        String nextState = currState.handleEvent(event, evtObj);
        if (nextState != null) {
            currState = getState(nextState);
            if (currState != null) {
                currState.onEntry(prevName, event, evtObj);
            } else {
                throw new CloudRuntimeException("Invalid next state: currState=" + prevName + ", evt=" + event + ", next=" + nextState);
            }
        } else {
            throw new CloudRuntimeException("Unhandled event transition: currState=" + prevName + ", evt=" + event);
        }
    }

    public Date getLastUpdated() {
        return lastUpdated;
    }

    public void setLastUpdated() {
        lastUpdated = new Date();
    }

    public void log(String message, Level level) {
        logger.log(level, message + ", " + type.toString() + " = " + typeName + " at host " + sserver);
    }

    public void setDisconnected() {
        transition(UploadEvent.DISCONNECT, null);
    }

    public void scheduleStatusCheck(com.cloud.agent.api.storage.UploadProgressCommand.RequestType getStatus) {
        if (statusTask != null)
            statusTask.cancel();

        statusTask = new StatusTask(this, getStatus);
        timer.schedule(statusTask, STATUS_POLL_INTERVAL);
    }

    public void scheduleTimeoutTask(long delay) {
        if (timeoutTask != null)
            timeoutTask.cancel();

        timeoutTask = new TimeoutTask(this);
        timer.schedule(timeoutTask, delay);
        if (logger.isDebugEnabled()) {
            log("Scheduling timeout at " + delay + " ms", Level.DEBUG);
        }
    }

    public void updateDatabase(Status state, String uploadErrorString) {
        resultObj.setResultString(uploadErrorString);
        resultObj.setState(state.toString());
        asyncMgr.updateAsyncJobAttachment(asyncJobId, type.toString(), 1L);
        asyncMgr.updateAsyncJobStatus(asyncJobId, JobInfo.Status.IN_PROGRESS.ordinal(), ApiSerializerHelper.toSerializedString(resultObj));

        UploadVO vo = uploadDao.createForUpdate();
        vo.setUploadState(state);
        vo.setLastUpdated(new Date());
        vo.setErrorString(uploadErrorString);
        uploadDao.update(getUploadId(), vo);
    }

    public void updateDatabase(Status state, String uploadUrl, String uploadErrorString) {
        resultObj.setResultString(uploadErrorString);
        resultObj.setState(state.toString());
        asyncMgr.updateAsyncJobAttachment(asyncJobId, type.toString(), 1L);
        asyncMgr.updateAsyncJobStatus(asyncJobId, JobInfo.Status.IN_PROGRESS.ordinal(), ApiSerializerHelper.toSerializedString(resultObj));

        UploadVO vo = uploadDao.createForUpdate();
        vo.setUploadState(state);
        vo.setLastUpdated(new Date());
        vo.setUploadUrl(uploadUrl);
        vo.setJobId(null);
        vo.setUploadPercent(0);
        vo.setErrorString(uploadErrorString);

        uploadDao.update(getUploadId(), vo);
    }

    private Long getUploadId() {
        return uploadId;
    }

    public synchronized void updateDatabase(UploadAnswer answer) {

        if (answer.getErrorString().startsWith("553")) {
            answer.setErrorString(answer.getErrorString().concat("Please check if the file name already exists."));
        }
        resultObj.setResultString(answer.getErrorString());
        resultObj.setState(answer.getUploadStatus().toString());
        resultObj.setUploadPercent(answer.getUploadPct());

        if (answer.getUploadStatus() == Status.UPLOAD_IN_PROGRESS) {
            asyncMgr.updateAsyncJobAttachment(asyncJobId, type.toString(), 1L);
            asyncMgr.updateAsyncJobStatus(asyncJobId, JobInfo.Status.IN_PROGRESS.ordinal(), ApiSerializerHelper.toSerializedString(resultObj));
        } else if (answer.getUploadStatus() == Status.UPLOADED) {
            resultObj.setResultString("Success");
            asyncMgr.completeAsyncJob(asyncJobId, JobInfo.Status.SUCCEEDED, 1, ApiSerializerHelper.toSerializedString(resultObj));
        } else {
            asyncMgr.completeAsyncJob(asyncJobId, JobInfo.Status.FAILED, 2, ApiSerializerHelper.toSerializedString(resultObj));
        }
        UploadVO updateBuilder = uploadDao.createForUpdate();
        updateBuilder.setUploadPercent(answer.getUploadPct());
        updateBuilder.setUploadState(answer.getUploadStatus());
        updateBuilder.setLastUpdated(new Date());
        updateBuilder.setErrorString(answer.getErrorString());
        updateBuilder.setJobId(answer.getJobId());

        uploadDao.update(getUploadId(), updateBuilder);
    }

    public void sendCommand(RequestType reqType) {
        if (getJobId() != null) {
            if (logger.isTraceEnabled()) {
                log("Sending progress command ", Level.TRACE);
            }
            try {
                EndPoint ep = _epSelector.select(sserver);
                if (ep == null) {
                    String errMsg = "No remote endpoint to send command, check if host or ssvm is down?";
                    logger.error(errMsg);
                    return;
                }
                ep.sendMessageAsync(new UploadProgressCommand(getCommand(), getJobId(), reqType), new Callback(ep.getId(), this));
            } catch (Exception e) {
                logger.debug("Send command failed", e);
                setDisconnected();
            }
        }

    }

    private UploadCommand getCommand() {
        return cmd;
    }

    public void logDisconnect() {
        logger.warn("Unable to monitor upload progress of {} at host {}", typeName, sserver);
    }

    public void scheduleImmediateStatusCheck(RequestType request) {
        if (statusTask != null)
            statusTask.cancel();
        statusTask = new StatusTask(this, request);
        timer.schedule(statusTask, SMALL_DELAY);
    }

    public void setCurrState(Status uploadState) {
        currState = getState(currState.toString());
    }

    public static class Callback implements AsyncCompletionCallback<Answer> {
        long id;
        Listener listener;

        public Callback(long id, Listener listener) {
            this.id = id;
            this.listener = listener;
        }

        @Override
        public void complete(Answer answer) {
            listener.processAnswers(id, -1, new Answer[] {answer});
        }
    }
}
