package ru.yandex.webmaster3.worker.task;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;

import ru.yandex.webmaster3.core.logbroker.reader.MessageContainer;
import ru.yandex.webmaster3.core.util.json.JsonMapping;
import ru.yandex.webmaster3.core.worker.client.LogbrokerMultiTopicClient;
import ru.yandex.webmaster3.core.worker.task.TaskResult;
import ru.yandex.webmaster3.core.worker.task.WorkerTaskData;
import ru.yandex.webmaster3.core.worker.task.WorkerTaskPriority;
import ru.yandex.webmaster3.core.worker.task.model.WorkerTaskDataBatch;
import ru.yandex.webmaster3.core.worker.task.model.WorkerTaskDataWrapper;
import ru.yandex.webmaster3.storage.task.TaskBatchLogYDao;
import ru.yandex.webmaster3.storage.task.TaskStateLogYDao;
import ru.yandex.webmaster3.storage.util.JsonDBMapping;
import ru.yandex.webmaster3.worker.Task;
import ru.yandex.webmaster3.worker.TaskRegistry;
import ru.yandex.webmaster3.worker.queue.TaskId;
import ru.yandex.webmaster3.worker.queue.TaskQueueMetrics;
import ru.yandex.webmaster3.worker.queue.TaskScheduler;

/**
 * ishalaru
 * 26.11.2019
 **/
@Value
@Slf4j
@RequiredArgsConstructor(onConstructor_ = {@Autowired})
public class InfinityWorkerRunner implements Runnable {
    private static final int WAITING_PERIOD_IN_MINUTES = 10;
    private static final ObjectMapper OM = JsonDBMapping.OM;
    BlockingQueue<MessageContainer> messageContainers;
    TaskQueueMetrics taskQueueMetrics;
    TaskRegistry taskRegistry;
    LogbrokerMultiTopicClient logBrokerMultiTopicClient;
    TaskBatchLogYDao taskBatchLogYDao;
    TaskStateLogYDao taskStateLogYDao;
    TaskScheduler taskScheduler;

    @Override
    public void run() {
        taskQueueMetrics.workerStarted(Thread.currentThread().getName());
        log.info("Start worker");
        while (!Thread.interrupted()) {
            try {
                final MessageContainer messageContainer = messageContainers.take();
                taskQueueMetrics.messageReaded();
                try {
                    List<byte[]> roundMessage = processMessages(messageContainer);
                    if (!roundMessage.isEmpty()) {
                        logBrokerMultiTopicClient.write(roundMessage, WorkerTaskPriority.LOW);
                        taskQueueMetrics.messageRollover();
                    }

                } catch (Exception exp) {
                    logBrokerMultiTopicClient.write(messageContainer.getRawMessages(), WorkerTaskPriority.LOW);
                    taskQueueMetrics.messageRollover();
                } finally {
                    messageContainer.commit();
                    taskQueueMetrics.messageCommited();
                }
            } catch (Exception exp) {
                log.error("{}", exp.getMessage(), exp);
            }
        }
        log.info("Stop worker");
        taskQueueMetrics.workerStopped(Thread.currentThread().getName());
    }


    private List<byte[]> processMessages(MessageContainer messageContainer) throws IOException {
        List<byte[]> roundMessage = new ArrayList<>(0);
        for (byte[] rawMessage : messageContainer.getRawMessages()) {
            final WorkerTaskDataBatch workerTaskDataBatch = workerTaskDataWrapperConvert(rawMessage);
            UUID batchId = workerTaskDataBatch.getUuid();
            TaskBatchLogYDao.Status status = taskBatchLogYDao.createOrStartBatch(batchId, workerTaskDataBatch.getList().size());
            switch (status) {
                case NEW:
                    processBatchInternal(roundMessage, workerTaskDataBatch, Collections.emptySet(), batchId);
                    break;
                case IN_PROGRESS:
                    //Если ранее данная пачка обрабатывалась, но по какой-то причине перестала, мы обрабатываем только те что ранее не обработывали
                    processBatchInternal(roundMessage, workerTaskDataBatch, new HashSet<>(taskStateLogYDao.list(workerTaskDataBatch.getUuid())), batchId);
                    break;
                case IN_PROGRESS_ON_CONTROL:
                    log.info("Batch {} on control. Current batch status: {}", batchId, status);
                    roundMessage.add(rawMessage);
                    break;
            }
        }
        return roundMessage;

    }

    private void processBatchInternal(List<byte[]> roundMessage, WorkerTaskDataBatch workerTaskDataBatch, Set<String> processedTasks, UUID batchId) throws IOException {
        List<WorkerTaskDataWrapper> list = new ArrayList<>();
        for (WorkerTaskDataWrapper workerTaskDataWrapper : workerTaskDataBatch.getList()) {
            if (!processedTasks.contains(workerTaskDataWrapper.getTaskId())) {
                if (processTask(workerTaskDataWrapper)) {
                    taskStateLogYDao.insert(batchId, workerTaskDataWrapper.getTaskId());
                } else {
                    list.add(workerTaskDataWrapper);
                }
            }
        }
        taskBatchLogYDao.update(batchId, TaskBatchLogYDao.Status.COMPLETED);
        if (!list.isEmpty()) {
            roundMessage.add(JsonMapping.writeValueAsString(new WorkerTaskDataBatch(list, UUID.randomUUID())).getBytes());
        }
    }

    private WorkerTaskDataBatch workerTaskDataWrapperConvert(byte[] raw) {
        try {
            final WorkerTaskDataBatch workerTaskDataBatch = OM.readValue(raw, WorkerTaskDataBatch.class);
            if (!(workerTaskDataBatch.getList() == null || workerTaskDataBatch.getList().isEmpty())) {
                return workerTaskDataBatch;
            }
        } catch (IOException e) {
            log.error("{}", e.getMessage(), e);
        }
        try {
            return new WorkerTaskDataBatch(List.of(OM.readValue(raw, WorkerTaskDataWrapper.class)), UUID.randomUUID());
        } catch (IOException e) {
            log.error("{}", e.getMessage(), e);
            throw new RuntimeException(e);
        }
    }

    private boolean processTask(WorkerTaskDataWrapper workerTaskDataWrapper) throws IOException {
        final Task task = taskRegistry.getTaskRegistryMap().get(workerTaskDataWrapper.getTaskType());

        if (task == null) {
            log.error("Undefined task type: {}", workerTaskDataWrapper);
            return true;
        }
        WorkerTaskData workerTaskData = (WorkerTaskData) OM.readValue(workerTaskDataWrapper.getTaskData(), task.getDataClass());
        TaskResult tr = TaskResult.FAIL;
        long startTime = System.nanoTime();
        boolean processed = true;
        try {
            log.info("Start task: type={} hostId={} {}",
                    workerTaskData.getTaskType(),
                    workerTaskData.getHostId(),
                    workerTaskData.getShortDescription());
            taskQueueMetrics.taskEnqueued(workerTaskData.getTaskType());
            taskQueueMetrics.taskPolled(workerTaskData.getTaskType());
            for (int i = 0; i < Math.max(1, workerTaskData.getRetryCount()); i++) {
                try {
                    if (!taskScheduler.isPaused(workerTaskData.getTaskType())) {
                        Task.Result result = task.runWithTracer(workerTaskData);
                        tr = result.getTaskResult();
                    } else {
                        processed = false;
                    }
                    break;
                } catch (Exception exp) {
                    log.error("{}", exp.getMessage(), exp);
                }
            }
            log.info("Finish task: type={} hostId={} result={} {} ",
                    workerTaskData.getTaskType(),
                    workerTaskData.getHostId(),
                    tr,
                    workerTaskData.getShortDescription());
        } catch (Exception e) {
            log.error("Error in task: type={} hostId={} {}", workerTaskData.getTaskType(),
                    workerTaskData.getHostId(), workerTaskData.getShortDescription(), e);
        }
        long taskRunTime = System.nanoTime() - startTime;
        final TaskId taskId = new TaskId(workerTaskData.getTaskType(), workerTaskData.getHostId(), workerTaskData.getTaskId());
        taskQueueMetrics.taskFinished(Thread.currentThread().getName(), taskId, taskRunTime, tr);
        return processed;
    }
}
