/*
 * Copyright (c) 2018 - 2023, Entgra (Pvt) Ltd. (http://www.entgra.io) All Rights Reserved.
 *
 * Entgra (Pvt) Ltd. 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 io.entgra.device.mgt.core.application.mgt.core.task;

import com.google.gson.Gson;
import io.entgra.device.mgt.core.application.mgt.common.SubAction;
import io.entgra.device.mgt.core.application.mgt.common.SubscriptionType;
import io.entgra.device.mgt.core.application.mgt.common.dto.ScheduledSubscriptionDTO;
import io.entgra.device.mgt.core.application.mgt.common.exception.SubscriptionManagementException;
import io.entgra.device.mgt.core.application.mgt.common.services.SubscriptionManager;
import io.entgra.device.mgt.core.application.mgt.core.exception.ApplicationOperationTaskException;
import io.entgra.device.mgt.core.application.mgt.core.impl.SubscriptionManagerImpl;
import io.entgra.device.mgt.core.application.mgt.core.internal.DataHolder;
import io.entgra.device.mgt.core.application.mgt.core.util.Constants;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.CronExpression;
import org.wso2.carbon.context.PrivilegedCarbonContext;
import org.wso2.carbon.ntask.common.TaskException;
import org.wso2.carbon.ntask.core.TaskInfo;
import org.wso2.carbon.ntask.core.TaskManager;
import org.wso2.carbon.ntask.core.service.TaskService;

import java.util.*;
import java.util.stream.Collectors;

public class ScheduledAppSubscriptionTaskManager {
    private static Log log = LogFactory.getLog(ScheduledAppSubscriptionTaskManager.class);
    private static final String SCHEDULED_APP_SUBSCRIPTION_TASK_TYPE = "SCHEDULED_APP_SUBSCRIPTION_TASK_TYPE";
    private static final String NAME_SEPARATOR = "_";
    private SubscriptionManager subscriptionManager;

    public ScheduledAppSubscriptionTaskManager() {
        this.subscriptionManager = new SubscriptionManagerImpl();
    }

    /**
     * Schedule a task to install/uninstall application for a list of subscribers
     *
     * @param applicationUUID  UUID of the application to install
     * @param subscribers      list of subscribers. This list can be of
     *                         either {@link io.entgra.device.mgt.core.device.mgt.common.DeviceIdentifier} if {@param subType} is
     *                         equal to DEVICE or {@link String} if {@param subType} is USER, ROLE or GROUP
     * @param subscriptionType subscription type. E.g. <code>DEVICE, USER, ROLE, GROUP</code>
     *                         {@see {@link SubscriptionType}}
     * @param action           action subscription action. E.g. {@code INSTALL/UNINSTALL}
     *                         {@see {@link SubAction}}
     * @param timestamp        timestamp to schedule the application subscription
     * @param properties         Properties sending to the device via operation
     * @param isOperationReExecutingDisabled To prevent adding the application subscribing
     *                                      already subscribed application successfully.
     * @throws ApplicationOperationTaskException if error occurred while scheduling the subscription
     */
    public void scheduleAppSubscriptionTask(String applicationUUID, List<?> subscribers,
                                            SubscriptionType subscriptionType, SubAction action, long timestamp,
                                            Properties properties, boolean isOperationReExecutingDisabled)
            throws ApplicationOperationTaskException {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date(timestamp * 1000));
        String space = " ";
        String cronExpression =
                calendar.get(Calendar.SECOND) + space + calendar.get(Calendar.MINUTE) + space
                + calendar.get(Calendar.HOUR_OF_DAY) + space + calendar.get(Calendar.DAY_OF_MONTH) + space
                + calendar.getDisplayName(Calendar.MONTH, Calendar.SHORT, Locale.getDefault()).toUpperCase() + " ? "
                + calendar.get(Calendar.YEAR);

        if (!CronExpression.isValidExpression(cronExpression)) {
            String msg = "The cron expression [" + cronExpression + "] generated by the" + " timestamp [" + timestamp
                    + "] is invalid";
            log.error(msg);
            throw new ApplicationOperationTaskException(msg);
        }

        try {
            PrivilegedCarbonContext carbonContext = PrivilegedCarbonContext.getThreadLocalCarbonContext();
            TaskService taskService = initializeTaskType();

            TaskManager taskManager = taskService.getTaskManager(SCHEDULED_APP_SUBSCRIPTION_TASK_TYPE);
            TaskInfo.TriggerInfo triggerInfo = new TaskInfo.TriggerInfo();
            triggerInfo.setCronExpression(cronExpression);
            triggerInfo.setRepeatCount(0);

            Map<String, String> taskProperties = new HashMap<>();
            taskProperties.put(Constants.SUB_TYPE, subscriptionType.toString());
            taskProperties.put(Constants.ACTION, action.toString());
            taskProperties.put(Constants.APP_UUID, applicationUUID);
            taskProperties.put(Constants.TENANT_DOMAIN, carbonContext.getTenantDomain(true));
            taskProperties.put(Constants.SUBSCRIBER, carbonContext.getUsername());
            taskProperties.put(Constants.OPERATION_RE_EXECUtING, String.valueOf(isOperationReExecutingDisabled));
            String subscribersString;
            if (SubscriptionType.DEVICE.equals(subscriptionType)) {
                subscribersString = new Gson().toJson(subscribers);
            } else {
                subscribersString = subscribers.stream().map(String.class::cast).collect(Collectors.joining(","));
            }
            taskProperties.put(Constants.SUBSCRIBERS, subscribersString);

            if(properties != null) {
                String payload = new Gson().toJson(properties);
                taskProperties.put(Constants.PAYLOAD, payload);
            }
            if (log.isDebugEnabled()) {
                log.debug("Scheduling a task to " + action.toString() + " application: " + applicationUUID +
                          " to/from the following " + subscriptionType.toString() + "S [" + subscribersString + "] at: "
                          + timestamp);
            }
            String taskName = subscriptionType.toString() + NAME_SEPARATOR + action.toString() + NAME_SEPARATOR
                              + DigestUtils.md5Hex(applicationUUID + NAME_SEPARATOR + subscribersString);
            taskProperties.put(Constants.TASK_NAME, taskName);
            TaskInfo taskInfo = new TaskInfo(taskName, ScheduledAppSubscriptionTask.class.getName(),
                    taskProperties, triggerInfo);

            ScheduledSubscriptionDTO subscriptionDTO = new ScheduledSubscriptionDTO(taskName, applicationUUID,
                    timestamp, subscribers, carbonContext.getUsername());
            subscriptionManager.createScheduledSubscription(subscriptionDTO);

            taskManager.registerTask(taskInfo);
            if (!taskManager.isTaskScheduled(taskName)) {
                taskManager.scheduleTask(taskName);
            } else {
                taskManager.rescheduleTask(taskName);
            }
        } catch (TaskException e) {
            String msg = "Error occurred while scheduling a task to " + action.toString() + " application: "
                    + applicationUUID + " to/from given " + subscriptionType.toString() + "S";
            log.error(msg, e);
            throw new ApplicationOperationTaskException(msg, e);
        } catch (SubscriptionManagementException e) {
            String msg = "Error occurred while writing the subscription to database";
            log.error(msg, e);
            throw new ApplicationOperationTaskException(msg, e);
        }
    }

    /**
     * Schedules a task to clean up the scheduled tasks from {@link org.wso2.carbon.ntask.core.TaskRepository}
     */
    public void scheduleCleanupTask() {
        try {
            TaskService taskService = initializeTaskType();
            TaskManager taskManager = taskService.getTaskManager(SCHEDULED_APP_SUBSCRIPTION_TASK_TYPE);

            TaskInfo.TriggerInfo triggerInfo = new TaskInfo.TriggerInfo();
            triggerInfo.setCronExpression("0 0 0 ? * * *");

            String taskName = "SCHEDULED_APP_SUBSCRIPTION_CLEANUP_TASK";
            TaskInfo taskInfo = new TaskInfo(taskName, ScheduledAppSubscriptionCleanupTask.class.getName(), null,
                    triggerInfo);

            taskManager.registerTask(taskInfo);
            if (!taskManager.isTaskScheduled(taskName)) {
                taskManager.scheduleTask(taskName);
            } else {
                taskManager.rescheduleTask(taskName);
            }
        } catch (TaskException e) {
            log.error("Error occurred while scheduling a cleanup task.");
        } catch (ApplicationOperationTaskException e) {
            log.error(e.getMessage());
        }
    }

    /**
     * Initialize task type.
     *
     * @return {@link TaskService}
     * @throws TaskException if error occurred while initializing task type
     * @throws ApplicationOperationTaskException if unable to load {@link TaskService}
     */
    private TaskService initializeTaskType() throws TaskException, ApplicationOperationTaskException {
        TaskService taskService = DataHolder.getInstance().getTaskService();
        if (taskService == null) {
            String msg = "Unable to load TaskService, hence unable to schedule the task.";
            log.error(msg);
            throw new ApplicationOperationTaskException(msg);
        }
        if (!taskService.getRegisteredTaskTypes().contains(SCHEDULED_APP_SUBSCRIPTION_TASK_TYPE)) {
            try {
                List<ScheduledSubscriptionDTO> removedTaskList =  subscriptionManager.cleanScheduledSubscriptions();
                removeScheduledSubscriptions(taskService, removedTaskList);
            } catch (SubscriptionManagementException e) {
                log.error("Error occurred while retrieving tasks to cleanup", e);
            }
            taskService.registerTaskType(SCHEDULED_APP_SUBSCRIPTION_TASK_TYPE);
        }
        return taskService;
    }

    /**
     * Cleans up already scheduled subscriptions.
     *
     * @param taskList list of {@link ScheduledSubscriptionDTO}s to remove
     */
    private void removeScheduledSubscriptions(TaskService taskService, List<ScheduledSubscriptionDTO> taskList) {
        try {
            TaskManager taskManager = taskService.getTaskManager(SCHEDULED_APP_SUBSCRIPTION_TASK_TYPE);
            taskManager.getAllTasks().forEach(
                    task -> taskList.stream().filter(t -> t.getTaskName().equals(task.getName())).forEach(t -> {
                        try {
                            taskManager.deleteTask(t.getTaskName());
                        } catch (TaskException e) {
                            log.error("Error occurred while removing the task: " + t.getTaskName(), e);
                        }
                    }));
        } catch (TaskException e) {
            log.error("Error occurred while removing tasks", e);
        }
    }
}
