package com.bytesw.bytebot.etl.batch.service;

import com.bytesw.bytebot.etl.batch.beans.DeleteDataSensBean;
import com.bytesw.bytebot.etl.batch.beans.DeleteDataSensControlBean;
import com.bytesw.bytebot.etl.batch.beans.DynaBean;
import com.bytesw.bytebot.etl.batch.beans.TenantBatchBean;
import com.bytesw.bytebot.etl.batch.factory.mapper.RowMapperFactory;
import com.bytesw.bytebot.etl.batch.factory.reader.DataBaseItemReaderFactory;
import com.bytesw.bytebot.etl.batch.processor.ConvertToBeanProcessor;
import com.bytesw.bytebot.etl.batch.writer.ByteBotJPAWriter;
import com.bytesw.bytebot.etl.batch.writer.DataSensibleJPAWriter;
import com.bytesw.bytebot.etl.config.BatchProperties;
import com.bytesw.bytebot.etl.dao.DeleteDataSensibleControlRepository;
import com.bytesw.bytebot.etl.dao.DeleteDataSensibleLogRepository;
import com.bytesw.bytebot.etl.dao.EventHistoryRepository;
import com.bytesw.bytebot.etl.dao.IntentRepository;
import com.bytesw.bytebot.etl.enums.IntentTypeEnum;
import com.bytesw.bytebot.etl.services.DeleteSensMessageService;
import com.bytesw.bytebot.etl.services.ProcessMessageService;
import com.bytesw.bytebot.jdbc.DeleteDataSensJDBCRepository;
import com.bytesw.bytebot.model.*;
import com.bytesw.bytebot.model.enums.AgentParameterEnum;
import com.bytesw.bytebot.model.enums.AgentStatusEnum;
import com.bytesw.bytebot.model.enums.FrequencyType;
import com.bytesw.bytebot.model.enums.ProcessETLEnum;
import com.bytesw.bytebot.repository.*;
import com.bytesw.bytebot.service.BusinessParameterService;
import com.bytesw.xdf.exception.NotFoundException;
import com.bytesw.xdf.multitenant.core.ThreadLocalStorage;
import com.google.gson.Gson;
import lombok.extern.log4j.Log4j2;
import net.javacrumbs.shedlock.core.DefaultLockingTaskExecutor;
import net.javacrumbs.shedlock.core.LockingTaskExecutor;
import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
import org.springframework.batch.core.*;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.item.ItemReader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZoneId;
import java.util.*;
import java.util.Calendar;
import java.util.concurrent.ScheduledFuture;

// Descomentar lo siguiente para que funcione
@Service
@Log4j2
@Component
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor="PT15M")
public class ScheduleService implements SchedulingConfigurer {

    ScheduledTaskRegistrar scheduledTaskRegistrar;

    Map<String, ScheduledFuture> futureMap = new HashMap<>();

    @Autowired
    @Qualifier("schedulerLockTaskScheduler")
    TaskScheduler poolScheduler;

    @Value("${application.byte-bot.batch.cron}")
    private String cronExpression;

    @Value("${application.byte-bot.batch.chunk:500}")
    private int chunk;

    @Value("${application.scheduler.cron}")
    private String cronScheduler;

    @Autowired
    private JobExplorer jobExplorer;

    @Autowired
    @Qualifier("asyncJobLauncher")
    JobLauncher asyncJobLauncher;

    @Autowired
    public JobBuilderFactory jobBuilderFactory;

    @Autowired
    public StepBuilderFactory stepBuilderFactory;

    @Autowired
    private DataBaseItemReaderFactory dataBaseItemReaderFactory;

    @Autowired
    private RowMapperFactory rowMapperFactory;

    @Autowired
    private JobExecutionListener listener;

    @Autowired
    private ProcessMessageService service;

    @Autowired
    private DeleteSensMessageService deleteSensMessageService;

    @Autowired
    private Gson gson;

    @Autowired
    private BatchProperties properties;

    @Autowired
    private EventHistoryRepository eventHistoryRepository;

    @Autowired
    private AgentRepository agentRepository;

    @Autowired
    private DeleteDataSensJDBCRepository deleteDataSensJDBCRepository;

    @Autowired
    private DeleteDataSensibleControlRepository deleteDataSensibleControlRepository;

    @Autowired
    private DeleteDataSensibleLogRepository deleteDataSensibleLogRepository;

    @Autowired
    private IntentRepository intentRepository;

    @Autowired
    private ProcessETLRepository processETLRepository;

    @Autowired
    private SchedulerTaskRepository schedulerTaskRepository;

    @Autowired
    private WeekSchedulerRepository weekSchedulerRepository;

    @Autowired
    private CalendarExceptionFullRepository calendarExceptionFullRepository;

    @Autowired
    private BusinessParameterService businessParameterService;

    private boolean schedulerFlag = false;

    private List<String> keys = new ArrayList<>();

    private String tenant;

    private ScheduledTaskRegistrar taskRegister;

    private TimeZone timeZone;

    @Value("${application.timezone.zone}")
    private String parameter;

    @Value("${application.timezone.default}")
    private String zoneDefault;

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        if (scheduledTaskRegistrar == null) {
            scheduledTaskRegistrar = taskRegistrar;
        }

        if (taskRegistrar.getScheduler() == null) {
            taskRegistrar.setScheduler(poolScheduler);
        }

        if (properties == null) {
            throw new RuntimeException("batch properties not found");
        }

        if (properties.getTenants() == null) {
            throw new RuntimeException("batch properties not found");
        }

        // identifier "prueba" Cambiar luego

        properties.getTenants().stream().forEach(tenant -> {
            ThreadLocalStorage.setTenantName(tenant.getId());
            configureTask(tenant.getId(), "prueba", taskRegistrar);
        });

        //@TODO Falta añadir el modo multitenant
    }

    private void configureTask(String tenantIdentifier, String identifier, ScheduledTaskRegistrar taskRegistrar) {
        tenant = tenantIdentifier;
        taskRegister = taskRegistrar;
        String zone = businessParameterService.getBusinessParameterForKey(parameter);
        if (zone != null) {
            timeZone = TimeZone.getTimeZone(zone);
        } else {
            timeZone = TimeZone.getTimeZone(zoneDefault);
        }

        /* Extraer el id del proceso - Delete */
        ProcessETL processDelete = processETLRepository.findByName(ProcessETLEnum.PROCESS_DELETE.getName());

        /* Busca las listas de SchedulerTask */
        List<SchedulerTask> listSchedulerTask = schedulerTaskRepository.findByEtlId(processDelete.getId());
        if (!listSchedulerTask.isEmpty()) {
            for (SchedulerTask schedulerTask : listSchedulerTask) {
                Trigger triggerDelete = new CronTrigger(schedulerTask.getCronExpression(), timeZone);
                String keyScheduler = String.format("%s-%s-%s", tenantIdentifier, schedulerTask.getCalendarID().trim(), schedulerTask.getId());
                futureMap.put(keyScheduler, taskRegistrar.getScheduler()
                        .schedule(() -> processDeleteData(tenantIdentifier, taskRegistrar, schedulerTask), triggerDelete));
            }
        } else {
            String keyScheduler = "refresh-schdeduler-task";
            Trigger triggerDelete = new CronTrigger(cronScheduler, timeZone);
            futureMap.put(keyScheduler, taskRegistrar.getScheduler()
                    .schedule(() -> syncSchedulerTask(), triggerDelete));
        }

    }

    private void syncSchedulerTask() {
        List<SchedulerTask> schedulerTasks = (List) schedulerTaskRepository.findAll();
        for (SchedulerTask model: schedulerTasks) {
            String key = String.format("%s-%s-%s", tenant, model.getCalendarID().trim(), model.getId());
            if (!futureMap.containsKey(key)) {
                Trigger trigger = new CronTrigger(model.getCronExpression(), timeZone);
                futureMap.put(key, taskRegister.getScheduler()
                        .schedule(() -> processDeleteData(tenant, taskRegister, model), trigger));
            }
        }
    }

    public void restartTask(SchedulerTask model) {
        String key = String.format("%s-%s-%s", tenant, model.getCalendarID().trim(), model.getId());
        Trigger trigger = new CronTrigger(model.getCronExpression(), timeZone);
        if (futureMap.containsKey(key)) {
            cancelTask(model);
        }
        futureMap.put(key, taskRegister.getScheduler()
                .schedule(() -> processDeleteData(tenant, taskRegister, model), trigger));
    }

    public void cancelTaskDelete(Long id) {
        String keyDataSens = String.format("deleteSensible-%s-%s", tenant, id);
        if (futureMap.containsKey(keyDataSens)) {
            ScheduledFuture future = futureMap.get(keyDataSens);
            future.cancel(true);
            futureMap.remove(keyDataSens);
            if (keys.contains(keyDataSens)) {
                keys.remove(keyDataSens);
            }
        }
    }

    public void cancelTask(SchedulerTask model) {
        String key = String.format("%s-%s-%s", tenant, model.getCalendarID().trim(), model.getId());
        if (futureMap.containsKey(key)){
            ScheduledFuture future = futureMap.get(key);
            future.cancel(true);
            futureMap.remove(key);
        }
        if (!keys.isEmpty()) {
            for (String keyTask: keys) {
                if (futureMap.containsKey(keyTask)) {
                    ScheduledFuture future = futureMap.get(keyTask);
                    future.cancel(true);
                    futureMap.remove(keyTask);
                }
            }
        }
    }

    /*ETL eliminacion de data sensible*/
    public void processDeleteData(String tenantIdentifier, ScheduledTaskRegistrar taskRegistrar, SchedulerTask schedulerTask) {
        String zone = businessParameterService.getBusinessParameterForKey(parameter);
        if (zone != null) {
            timeZone = TimeZone.getTimeZone(zone);
        } else {
            timeZone = TimeZone.getTimeZone(zoneDefault);
        }

        Optional<SchedulerTask> schedulerTaskActual = schedulerTaskRepository.findById(schedulerTask.getId());
        if (schedulerTaskActual.isPresent()) {
            schedulerTask = schedulerTaskActual.get();
        } else {
            log.info(String.format("No se encuentra la tarea de %s regristrado.", schedulerTask.getDescription()));
            cancelTask(schedulerTask);
            return;
        }

        String calendarId = schedulerTask.getCalendarID().trim();
        Optional<List<WeekScheduler>> dates = weekSchedulerRepository.findByCalendarId(calendarId);
        OffsetTime actual = OffsetTime.now();
        OffsetDateTime now = OffsetDateTime.now();
        for(WeekScheduler weekScheduler: dates.get()) {
            int day = now.getDayOfWeek().getValue() == 7? 1: (now.getDayOfWeek().getValue() + 1);
            if(day != weekScheduler.getDayOfWeek()){
                schedulerFlag = false;
                continue;
            }
            if (actual.isBefore(weekScheduler.getTo()) && actual.isAfter(weekScheduler.getFrom())) {
                Optional<List<CalendarExceptionFull>> calendarException = calendarExceptionFullRepository.findByCalendarId(calendarId);
                if (calendarException.isPresent()){
                    schedulerFlag = validateException(calendarException.get());
                } else {
                    schedulerFlag = true;
                }
                break;
            } else {
                schedulerFlag = false;
            }
        }

        if (schedulerFlag) {
            List<DeleteDataSensBean> deleteDataSensBeans = deleteDataSensJDBCRepository
                    .getListAgentChannel(AgentStatusEnum.DEPLOYED.getName(), AgentParameterEnum.ACCESS_TWILIO.getName());
            String keyDataSens = "";
            for (DeleteDataSensBean data : deleteDataSensBeans) {
                Trigger trigger = new CronTrigger(cronExpression, timeZone);
                keyDataSens = String.format("deleteSensible-%s-%s", tenantIdentifier, data.getAgenId());
                if (!futureMap.containsKey(keyDataSens)) {
                    keys.add(keyDataSens);
                    futureMap.put(keyDataSens, taskRegistrar.getScheduler()
                            .schedule(() -> scheduleCron(createJobDataSens(data), tenantIdentifier), trigger));
                }
            }
            deleteDataSensBeans = deleteDataSensJDBCRepository.getListAgentChannel(AgentStatusEnum.DELETED.getName(), AgentParameterEnum.ACCESS_TWILIO.getName());
            if (!deleteDataSensBeans.isEmpty()) {
                for (DeleteDataSensBean data : deleteDataSensBeans) {
                    cancelTaskDelete(data.getAgenId());
                }
            }
        } else {
            keys.forEach(x -> {
                if (futureMap.containsKey(x)) {
                    ScheduledFuture future = futureMap.get(x);
                    future.cancel(true);
                    futureMap.remove(x);
                }
            });
        }
    }

    private boolean validateException(List<CalendarExceptionFull> calendarException){
        boolean result = true;
        FrequencyType[] frecuencyType =  FrequencyType.values();
        OffsetDateTime now = OffsetDateTime.now();
        for (CalendarExceptionFull exception : calendarException){
            for (int i = 0; i < frecuencyType.length; i++) {
                if (frecuencyType[i].getName().equals(exception.getFrequencyType())) {
                    if (exception.getFrequencyType().equals(FrequencyType.ANNUAL.getName())) {
                        if (now.getMonth().getValue() == exception.getMonth().intValue() && exception.getDayOfMonth().intValue() == now.getDayOfMonth()){
                            result = false;
                        }
                    } else if (exception.getFrequencyType().equals(FrequencyType.RANGE.getName())) {
                        if (now.isBefore(exception.getTo()) && now.isAfter(exception.getFrom())){
                            result = false;
                        }
                    } else if (exception.getFrequencyType().equals(FrequencyType.UNIQUE.getName())) {
                        if (now.toLocalDate().equals(exception.getDate())) {
                            result = false;
                        }
                    } else if (exception.getFrequencyType().equals(FrequencyType.UniqueWeekly.getName())) {
                        Calendar ca1 = Calendar.getInstance();
                        ca1.set(now.getYear(), now.getMonth().getValue(), now.getDayOfMonth());
                        ca1.setMinimalDaysInFirstWeek(1);
                        if (now.getMonth().getValue() == exception.getMonth().intValue() && exception.getDayOfWeek().intValue() == now.getDayOfWeek().getValue()
                                && exception.getWeekOfMonth().intValue() == ca1.WEEK_OF_MONTH){
                            result = false;
                        }
                    }
                    break;
                }
            }
            if (!result) {
                break;
            }
        }
        return result;
    }

    /* Métodos utilizados para ETL de dashboard */
    @Scheduled(cron = "${application.byte-bot.batch.cron}")
    @SchedulerLock(name = "Dashboard-ETL", lockAtLeastFor = "PT40S", lockAtMostFor = "PT50S")
    protected void createJob() {

        List<Agent> agentDeployed = agentRepository.findByStatus(AgentStatusEnum.DEPLOYED);
        if (!agentDeployed.isEmpty()) {
            ThreadLocalStorage.setTenantName(tenant);
            Job job = jobBuilderFactory.get("processJob")
                    .incrementer(new RunIdIncrementer()).listener(listener)
                    .flow(createStep(tenant)).end().build();
            scheduleCron(job, tenant);
        } else {
            log.info("No hay agentes deployados.");
        }
    }

    private Step createStep(String tenantIdentifier) {
        ByteBotJPAWriter writer = new ByteBotJPAWriter();
        writer.setService(service);
        return stepBuilderFactory.get("processByteBotData").<DynaBean, DynaBean>chunk(chunk)
                .reader(getReader(tenantIdentifier))
                .processor(new ConvertToBeanProcessor())
                .writer(writer).build();
    }

    private ItemReader getReader(String tenantIdentifier) {
        if (properties == null) {
            throw new RuntimeException("Properties not found");
        }

        Optional<TenantBatchBean> tenantFound = findTenant(tenantIdentifier);
        if (!tenantFound.isPresent()) {
            throw new RuntimeException("Properties not found");
        }

        Long id = eventHistoryRepository.maxEventId();
        Map<String, Object> params = new HashMap<>();
        params.putAll(tenantFound.get().getDatasource());
        params.put("query", String.format((String) params.get("query"), id != null ? id : 0));

        return dataBaseItemReaderFactory.createReader(rowMapperFactory.createMapper(new HashMap<>()), params);
    }

    /* Métodos ETL de data sensible */
    private Job createJobDataSens(DeleteDataSensBean data) {
        log.info("ETL de eliminacion");
        data = deleteDataSensJDBCRepository.getAgentChannelByAgentID(AgentStatusEnum.DEPLOYED.getName(),
                AgentParameterEnum.ACCESS_TWILIO.getName(), data.getAgenId());
        ThreadLocalStorage.setTenantName(tenant);
        if (data == null) {
            return null;
        }
        return jobBuilderFactory.get( String.format("processDataSensible-%d",data.getAgenId()))
                .incrementer(new RunIdIncrementer()).listener(listener)
                .flow(createStepDataSens(tenant, data)).end().build();
    }

    private Step createStepDataSens(String tenantIdentifier, DeleteDataSensBean data) {
        DataSensibleJPAWriter writer = new DataSensibleJPAWriter();
        List<String> intentByAgent = deleteDataSensJDBCRepository.getIntentByAgent(IntentTypeEnum.SENSIBLE.getName(), data.getAgenId());
        writer.setIntentByAgent(intentByAgent);
        writer.setAgent(data);
        writer.setService(deleteSensMessageService);
        writer.setIntentRepository(intentRepository);
        writer.setDeleteDataSensibleControlRepository(deleteDataSensibleControlRepository);
        writer.setDeleteDataSensibleLogRepository(deleteDataSensibleLogRepository);

        return stepBuilderFactory.get("processByteBotDatasSens")
                .<DynaBean, DynaBean>chunk(chunk)
                .reader(getReaderDataSens(tenantIdentifier, data))
                .processor(new ConvertToBeanProcessor())
                .writer(writer).build();
    }

    private ItemReader getReaderDataSens(String tenantIdentifier, DeleteDataSensBean data) {
        if (properties == null) {
            throw new RuntimeException("Properties not found");
        }

        Optional<TenantBatchBean> tenantFound = findTenant(tenantIdentifier);
        if (!tenantFound.isPresent()) {
            throw new RuntimeException("Properties not found");
        }

        DeleteDataSensControlBean control = deleteDataSensJDBCRepository.getControl(data.getAgenId());
        OffsetDateTime endDate = OffsetDateTime.now();
        Long id = 0L;
        Long differenceTime = 0L;

        if (control != null) {
            OffsetDateTime lastSessionDate = OffsetDateTime.ofInstant(control.getDateDelete().toInstant(), ZoneId.systemDefault());
            Duration difference = Duration.between(lastSessionDate, endDate);

            differenceTime = (difference.getSeconds() / 60);

            if (differenceTime < data.getDeletePeriod()) {
                log.info("Agente " + data.getValue() + " aun no cumple con el periodo de espera.");
                id = null;
            } else {
                id = control.getEventId();
            }
        }

        Map<String, Object> params = new HashMap<>();
        params.putAll(tenantFound.get().getDatasource());
        params.put("query", String.format((String) params.get("query"), id));

        return dataBaseItemReaderFactory.createReader(rowMapperFactory.createMapper(new HashMap<>()), params);
    }

    /* Métodos generales */

    private Optional<TenantBatchBean> findTenant(String tenantId) {
        return properties.getTenants().stream().filter(x -> x.getId().equals(tenantId)).findFirst();
    }


    private void scheduleCron(Job job, String tenantId) {
        UUID traceID = UUID.randomUUID();
        Map<String, JobParameter> maps = new HashMap<>();
        maps.put("time", new JobParameter(System.currentTimeMillis()));
        maps.put("uuid", new JobParameter(traceID.toString()));
        maps.put("tenantId", new JobParameter(tenantId));
        JobParameters parameters = new JobParameters(maps);
        Set<JobExecution> jobExecutions = new HashSet<>();
        try {
            if (job != null) {
                jobExecutions = jobExplorer.findRunningJobExecutions(job.getName());
                String JobInfo = String.format("Jobs {} en Ejecución: {}", job.getName(), jobExecutions.size());
                log.info(JobInfo);
                if (jobExecutions.isEmpty()) {
                    asyncJobLauncher.run(job, parameters);
                } else {
                    log.info("El Job " + job.getName() + " no se ejecutará porque hay jobs pendientes: " + jobExecutions.size());
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
