package id.iptek.utms.agent.scheduler.job;

import com.google.gson.Gson;
import id.iptek.utms.agent.AppConfig;
import id.iptek.utms.agent.dao.ApplicationDao;
import id.iptek.utms.agent.dao.DownloadTaskDao;
import id.iptek.utms.agent.model.ApplicationExt;
import id.iptek.utms.agent.model.DownloadTask;
import id.iptek.utms.agent.model.PendingDownloadTask;
import id.iptek.utms.agent.model.enumeration.TaskStatus;
import id.iptek.utms.agent.util.Singleton;
import io.minio.GetPresignedObjectUrlArgs;
import io.minio.MinioClient;
import io.minio.http.Method;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import org.apache.commons.codec.digest.DigestUtils;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 * @author jakar
 */
public class DownloadTaskPublisherJob implements Job {

    private final Logger logger = LoggerFactory.getLogger(getClass());
    private ApplicationDao appDao = new ApplicationDao();
    private DownloadTaskDao dao = new DownloadTaskDao();
    private static final SimpleDateFormat dateTimeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
    private static final SimpleDateFormat sdfYEAR = new SimpleDateFormat("yyyy");
    private static final SimpleDateFormat sdfMONTH = new SimpleDateFormat("MM");
    private static final SimpleDateFormat sdfDATE = new SimpleDateFormat("dd");
    
    public DownloadTaskPublisherJob() {
    }

    @Override
    public void execute(JobExecutionContext jec) throws JobExecutionException {
        try {
            logger.info("Execute publish task job ..");
            JobDataMap dataMap = jec.getMergedJobDataMap();
            // get task
            DownloadTask task = (DownloadTask) dataMap.get("task");
            logger.info(">> {}", task);
            if(task.getStatus() == TaskStatus.INITIAL) {
                boolean populated = dao.populateAndBuildDownloadTaskLogs(task);
                logger.info("Populated? {}", populated);
            }
            List<PendingDownloadTask> groupPendingDownloadTasks = dao.getUnpublishedPendingTaskByTerminalGroup(task);
            List<PendingDownloadTask> pendingDownloadTasks = dao.getUnpublishedPendingTaskByTerminal(task);
            
            MqttClient mqttClient = (MqttClient) Singleton.getInstance("MQTT_CLIENT").getObject();
            logger.info("Total Group Pending Download Tasks: {}", groupPendingDownloadTasks.size());
            for(PendingDownloadTask downloadTask : groupPendingDownloadTasks) {
                logger.info(">> [1] task={}, group={}, app={}", downloadTask.getId(), downloadTask.getTerminalGroupId(), downloadTask.getApp().getId());
                broadcastDownloadTasksToDevice(mqttClient, downloadTask);
            }
            logger.info("Total Pending Download Tasks: {}", pendingDownloadTasks.size());
            for(PendingDownloadTask downloadTask : pendingDownloadTasks) {
                logger.info(">> [2] task={}, terminal={}, app={}", downloadTask.getId(), downloadTask.getTerminalSN(), downloadTask.getApp().getId());
                broadcastDownloadTasksToDevice(mqttClient, downloadTask);
            }
        } catch(Exception ex) {
            logger.error("Error publish task: {}", ex.getMessage(), ex);
        } finally {}
    }
    
    // send download task to device topic via mqtt
    protected void broadcastDownloadTasksToDevice(MqttClient mqttClient,
            PendingDownloadTask task) throws Exception {
        logger.debug("Broadcast tasks ...");
        try {
            int qos = 2;
            String topicName;
            // broadcast to group or specific SN
            boolean broadcastToGroup = false;
            if(task.getTerminalGroupId() != null) {
                topicName = task.getTerminalGroupId();
                broadcastToGroup = true;
            } else {
                topicName = task.getTerminalSN().toUpperCase() + "_IN";
            }
            
            AppConfig appConfig = AppConfig.getInstance();
            Date now = Calendar.getInstance().getTime();
            ApplicationExt app = (ApplicationExt) task.getApp();
            String bucketName = appConfig.get("fileserver.bucket");
            // check icon url expiry
            Date iconUrlExp = app.getIconUrlExp();
            if(iconUrlExp == null || now.after(iconUrlExp)) {
                logger.debug("Icon url expired: {}", iconUrlExp);
                String iconUniqueName = app.getUniqueIconName();
                
                String path = appConfig.get("fileserver.icon.path");
                int expiry = appConfig.getAsInt("fileserver.icon.expiry", 7);
                TimeUnit timeUnit = getTimeUnit(appConfig, "fileserver.icon.timeunit", "server.icon.timeunit", "DAYS");
                Date iconExpiryDate = getExpiryDate(now, expiry, timeUnit);
                
                logger.info("Generate icon url for app '{}', old expiry: {}", app.getId(), iconUrlExp);
                String iconDownloadUrl = generateDownloadUrl(bucketName, path, iconUniqueName,
                        expiry, timeUnit);
                app.setIconUrl(iconDownloadUrl);
                app.setIconUrlExp(iconExpiryDate);
                logger.info("Icon url generated for app '{}', new expiry: {}", app.getId(), iconExpiryDate);
                
                boolean iconUrlUpdated = appDao.updateIconUrl(app);
                logger.info("Icon url updated? {}", iconUrlUpdated);
            }
            // check download url expiry
            Date downloadUrlExp = app.getDownloadUrlExp();
            if(downloadUrlExp == null || now.after(downloadUrlExp)) {
                logger.info("Download app url expired for task '{}', app '{}', old expiry: {}", task.getId(), app.getId(), downloadUrlExp);
                String uniqueName = app.getUniqueName();
                
                String path = appConfig.get("fileserver.app.path");
                int expiry = appConfig.getAsInt("fileserver.app.expiry", 7);
                TimeUnit timeUnit = getTimeUnit(appConfig, "fileserver.app.timeunit", "server.app.timeunit", "DAYS");
                Date appExpiryDate = getExpiryDate(now, expiry, timeUnit);
                
                logger.info("Generate app url for task '{}', app '{}'", task.getId(), app.getId());
                String appDownloadUrl = generateDownloadUrl(bucketName, path, uniqueName,
                        expiry, timeUnit);
                app.setDownloadUrl(appDownloadUrl);
                app.setDownloadUrlExp(appExpiryDate);
                logger.info("App url generated for task '{}', app '{}', new expiry: {}", task.getId(), app.getId(), appExpiryDate);
                
                boolean appUrlUpdated = appDao.updateAppUrl(task, app);
                logger.info("App url updated? {}", appUrlUpdated);
            }
            
            // generate json
            Gson gson = new Gson();
            Map messageMap = new HashMap<>();
            messageMap.put("req_id", UUID.randomUUID().toString());
            messageMap.put("req_time", dateTimeFormat.format(new Date()));
            // we'll use log's id as ack_id
            if(broadcastToGroup) {
                String ackId = "BROADCAST_" + task.getId();
                messageMap.put("ack_id", ackId);
            } else {
                messageMap.put("ack_id", task.getLogId());
            }
            messageMap.put("req_type", "DOWNLOAD_APP_TASK");
            // task map
            Map taskMap = new HashMap<>();
            taskMap.put("id", task.getId());
            taskMap.put("name", task.getName());
            taskMap.put("download_time_type", getDownloadTimeTypeName(task.getDownloadTimeType()));
            if (task.getDownloadTimeType() == 2) {
                taskMap.put("download_time", dateTimeFormat.format(task.getDownloadTime()));
            }
            taskMap.put("installation_time_type", getInstallationTimeTypeName(task.getInstallationTimeType()));
            if (task.getInstallationTimeType() == 2) {
                taskMap.put("installation_time", dateTimeFormat.format(task.getInstallationTime()));
            }
            taskMap.put("installation_notification", getInstallationNotificationName(task.getInstallationNotification()));
            messageMap.put("task", taskMap);
            // app map
            Map appMap = new HashMap<>();
            appMap.put("id", app.getId());
            appMap.put("name", app.getAppName());
            appMap.put("package_name", app.getPackageName());
            appMap.put("version", app.getVersion());
            appMap.put("company", app.getCompanyName());
            appMap.put("uninstallable", app.isUninstallable());
            appMap.put("description", app.getDescription());
            appMap.put("download_url", app.getDownloadUrl());
            appMap.put("md5_checksum", app.getChecksum());
            messageMap.put("app", appMap);

            String messageData = gson.toJson(messageMap);
            logger.debug("Try publish message to: {}", topicName);
            logger.debug(">> {}", messageData);
            mqttClient.publish(topicName, messageData.getBytes(), qos, false);
            logger.debug("Message published!");

            // change log's last broadcast time
            if(broadcastToGroup) {
                boolean updated = dao.updateGroupLastBroadcastTs(task);
                logger.debug("Download task group last broadcast time updated? {}", updated);
            } else {
                boolean updated = dao.updateTerminalLastBroadcastTs(task);
                logger.debug("Download task terminal last broadcast time updated? {}", updated);
            }
        } finally {
            logger.debug("Start broadcast download tasks DONE");
        }
    }
    
    
    
    private String getDownloadTimeTypeName(int type) {
        switch (type) {
            case 1:
                return "NEXT_CONTACT";
            case 2:
                return "DATETIME";
        }
        return null;
    }

    private String getInstallationTimeTypeName(int type) {
        switch (type) {
            case 1:
                return "IMMEDIATE_AFTER_DOWNLOAD";
            case 2:
                return "DATETIME";
        }
        return null;
    }

    private String getInstallationNotificationName(int type) {
        switch (type) {
            case 1:
                return "SILENT";
            case 2:
                return "NEED_CONFIRMATION";
        }
        return null;
    }

    private String getFilePath(String fileId, String fileExt, Date fileCreateDate) {
        String filePath = AppConfig.getInstance().get("file.dir") + File.separator
                + sdfYEAR.format(fileCreateDate) + File.separator
                + sdfMONTH.format(fileCreateDate) + File.separator
                + sdfDATE.format(fileCreateDate) + File.separator
                + fileId + "." + fileExt;
        return filePath;
    }

    private String md5Checksum(byte[] bytes) throws Exception {
        try {
            String checksum = DigestUtils.md5Hex(bytes);
            return checksum;
        } finally {
        }
    }
    
    private String generateDownloadUrl(String bucketName, String path, String uniqueName, int expiry, TimeUnit expiryTimeUnit) throws Exception {
        MinioClient minioClient = (MinioClient) Singleton.getInstance("MINIO_CLIENT").getObject();
        String url
                = minioClient.getPresignedObjectUrl(
                        GetPresignedObjectUrlArgs.builder()
                                .method(Method.GET)
                                .bucket(bucketName)
                                .object(path + "/" + uniqueName)
                                .expiry(expiry, expiryTimeUnit)
                                .build());
        return url;
    }
    
    private Date getExpiryDate(Date now, int expiry, TimeUnit timeUnit) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(now);
        switch (timeUnit) {
            case MILLISECONDS:
                cal.add(Calendar.MILLISECOND, expiry);
                break;
            case SECONDS:
                cal.add(Calendar.SECOND, expiry);
                break;
            case MINUTES:
                cal.add(Calendar.MINUTE, expiry);
                break;
            case HOURS:
                cal.add(Calendar.HOUR_OF_DAY, expiry);
                break;
            case DAYS:
                cal.add(Calendar.DAY_OF_MONTH, expiry);
                break;
        }
        return cal.getTime();
    }

    private TimeUnit getTimeUnit(AppConfig appConfig, String name, String legacyName, String defaultValue) {
        String value = appConfig.get(name);
        if(value == null) {
            value = appConfig.get(legacyName, defaultValue);
        }
        return TimeUnit.valueOf(value.trim().toUpperCase());
    }
}
