package org.hso.ecommerce.controller.cronjob;

import org.hso.ecommerce.entities.cron.BackgroundJob;
import org.hso.ecommerce.repos.booking.BookingAccountEntryRepository;
import org.hso.ecommerce.repos.booking.BookingRepository;
import org.hso.ecommerce.repos.cronjob.BackgroundJobRepository;
import org.hso.ecommerce.repos.dashboard.DashboardSummaryRepository;
import org.hso.ecommerce.repos.shop.ArticleRepository;
import org.hso.ecommerce.repos.shop.CustomerOrderRepository;
import org.hso.ecommerce.repos.supplier.ArticleOfferRepository;
import org.hso.ecommerce.repos.supplier.SupplierOrderRepository;
import org.hso.ecommerce.repos.supplier.SupplierRepository;
import org.hso.ecommerce.repos.warehouse.WarehouseBookingPositionSlotEntryRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.annotation.PostConstruct;
import java.sql.Timestamp;
import java.util.*;
import java.util.Map.Entry;

@Component
@RequestMapping("intern/cronjobs")
class CronjobController {
    private static final Logger log = LoggerFactory.getLogger(CronjobController.class);

    private Map<String, ICronjob> cronjobs;

    @Autowired
    private final BackgroundJobRepository cronjobRepository = null;

    @Autowired
    final ArticleRepository articleRepository = null;

    @Autowired
    final ArticleOfferRepository articleOfferRepository = null;

    @Autowired
    final CustomerOrderRepository customerOrderRepository = null;

    @Autowired
    final BookingRepository bookingRepository = null;

    @Autowired
    final BookingAccountEntryRepository bookingAccountEntryRepository = null;

    @Autowired
    final WarehouseBookingPositionSlotEntryRepository warehouseBookingPositionSlotEntryRepository = null;

    @Autowired
    final DashboardSummaryRepository dashboardSummaryRepository = null;

    @Autowired
    final SupplierRepository supplierRepository = null;

    @Autowired
    final SupplierOrderRepository supplierOrderRepository = null;

    @Autowired
    final Reorder reorderJob = null;

    @Autowired
    final DashboardCronjob dashboardCronjob = null;

    @Autowired
    final AutoSupplierPayment autoSupplierPaymentJob = null;

    @PostConstruct
    public void init() {
        cronjobs = getCronjobs();
    }

    private Map<String, ICronjob> getCronjobs() {
        HashMap<String, ICronjob> map = new HashMap<>();

        // Register all existing cronjobs
        map.put(BackgroundJob.JOB_REORDER, reorderJob);
        map.put(BackgroundJob.JOB_DASHBOARD, dashboardCronjob);
        map.put(BackgroundJob.JOB_SUPPLIER_AUTO_PAYMENT, autoSupplierPaymentJob);

        return Collections.unmodifiableMap(map);
    }

    private ScheduledCronjob getNextCronjob() {
        Calendar currentTime = new GregorianCalendar();
        Iterable<BackgroundJob> jobs = cronjobRepository.findAll();
        HashMap<String, BackgroundJob> alreadyExecuted = new HashMap<>();
        for (BackgroundJob job : jobs) {
            alreadyExecuted.put(job.jobName, job);
        }
        ScheduledCronjob earliestJob = null;
        for (Entry<String, ICronjob> entry : cronjobs.entrySet()) {
            ScheduledCronjob resultingJob;
            BackgroundJob dbEntry = alreadyExecuted.get(entry.getKey());
            if (dbEntry != null) {
                Calendar previousExecution = new GregorianCalendar();
                previousExecution.setTimeInMillis(dbEntry.lastExecution.getTime());
                Calendar followingSchedule = entry.getValue().nextExecution((Calendar) previousExecution.clone());
                Calendar lastSchedule = entry.getValue().previousExecution((Calendar) currentTime.clone());
                if (lastSchedule.getTimeInMillis() > followingSchedule.getTimeInMillis()) {
                    // This happens, if more than one execution was missed.
                    // In this case, run the job only once.
                    followingSchedule = lastSchedule;
                }
                resultingJob = new ScheduledCronjob(followingSchedule, entry.getValue(), dbEntry);
            } else {
                // This cronjob has never been executed before.
                Calendar lastScheduleTime = entry.getValue().previousExecution((Calendar) currentTime.clone());
                BackgroundJob model = new BackgroundJob();
                model.jobName = entry.getKey();
                resultingJob = new ScheduledCronjob(lastScheduleTime, entry.getValue(), model);
            }

            // Look for the job with earliest executionTime - it will run next
            if (earliestJob == null
                    || resultingJob.executionTime.getTimeInMillis() < earliestJob.executionTime.getTimeInMillis()) {
                earliestJob = resultingJob;
            }
        }
        return earliestJob;
    }

    private void runCronjobExecutionLoop() {
        Thread.currentThread().setName("Cronjob");
        try {
            while (true) {
                ScheduledCronjob nextJob = getNextCronjob();
                if (nextJob == null) {
                    // In case there are no cronjobs
                    return;
                }
                long waitingTime = nextJob.executionTime.getTimeInMillis() - System.currentTimeMillis();
                if (waitingTime > 0) {
                    Thread.sleep(waitingTime);
                }

                try {
                    nextJob.cronjob.executeAt(nextJob.executionTime, this);
                } catch (Throwable t) {
                    log.error("Failed to execute cronjob " + nextJob.cronjob.getClass() + ":");
                    t.printStackTrace();
                }

                nextJob.model.lastExecution = new Timestamp(nextJob.executionTime.getTimeInMillis());
                cronjobRepository.save(nextJob.model);
            }
        } catch (InterruptedException e) {
            log.error("The cronjob execution thread has been interrupted");
        }
    }

    @PostConstruct
    public void onPostConstruct() {
        new Thread(this::runCronjobExecutionLoop).start();
    }

    class ManualCronjob {
        public final String identifier;
        public final String visibleName;

        public ManualCronjob(String identifier, String visibleName) {
            this.identifier = identifier;
            this.visibleName = visibleName;
        }
    }

    private List<ManualCronjob> listAllCronjobs() {
        ArrayList<ManualCronjob> entries = new ArrayList<>();
        for (Entry<String, ICronjob> job : cronjobs.entrySet()) {
            entries.add(new ManualCronjob(job.getKey(), job.getValue().getDisplayName()));
        }
        return entries;
    }

    @GetMapping("/")
    public String cronjobOverview(Model model) {
        model.addAttribute("jobs", listAllCronjobs());
        return "intern/cronjobs/index";
    }

    @PostMapping("/run/{identifier}")
    public String runCronjob(Model model, @PathVariable("identifier") String identifier) {
        ICronjob jobToExecute = cronjobs.get(identifier);
        if (jobToExecute != null) {
            jobToExecute.executeAt(new GregorianCalendar(), this);
            model.addAttribute("info",
                    String.format("Der Cronjob \"%s\" wurde ausgeführt.", jobToExecute.getDisplayName()));
        } else {
            model.addAttribute("error", "Der Cronjob konnte nicht gefunden werden.");
        }
        model.addAttribute("jobs", listAllCronjobs());
        return "intern/cronjobs/index";
    }
}
