package org.hso.ecommerce.controller.cronjob;

import org.hso.ecommerce.action.booking.CreateBookingAction;
import org.hso.ecommerce.action.cronjob.ReadSupplierDataAction;
import org.hso.ecommerce.action.cronjob.ReorderAction;
import org.hso.ecommerce.action.cronjob.UpdateOffersAction;
import org.hso.ecommerce.entities.booking.Booking;
import org.hso.ecommerce.entities.booking.BookingAccountEntry;
import org.hso.ecommerce.entities.booking.BookingReason;
import org.hso.ecommerce.entities.shop.Article;
import org.hso.ecommerce.entities.supplier.ArticleOffer;
import org.hso.ecommerce.entities.supplier.Supplier;
import org.hso.ecommerce.entities.supplier.SupplierOrder;
import org.springframework.stereotype.Component;

import java.sql.Timestamp;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;

@Component
class Reorder implements ICronjob {
    @Override
    public String getDisplayName() {
        return "Nachbestellung";
    }

    @Override
    public Calendar nextExecution(Calendar reference) {
        if (reference.get(Calendar.HOUR_OF_DAY) >= 8) {
            reference.add(Calendar.DAY_OF_MONTH, 1);
        }
        reference.set(Calendar.HOUR_OF_DAY, 8);
        reference.set(Calendar.MINUTE, 0);
        reference.set(Calendar.SECOND, 0);
        reference.set(Calendar.MILLISECOND, 0);
        return reference;
    }

    @Override
    public Calendar previousExecution(Calendar reference) {
        if (reference.get(Calendar.HOUR_OF_DAY) < 8) {
            reference.add(Calendar.DAY_OF_MONTH, -1);
        }
        reference.set(Calendar.HOUR_OF_DAY, 8);
        reference.set(Calendar.MINUTE, 0);
        reference.set(Calendar.SECOND, 0);
        reference.set(Calendar.MILLISECOND, 0);
        return reference;
    }

    /**
     * Calculates the amount of ordered articles by customers for the given article type in the time between begin and
     * end.
     *
     * @param article The article to search orders for.
     * @param begin   The start time for the search (included)
     * @param end     The end time for the search (excluded)
     * @return The number of articles that were ordered by customers in the given range.
     */
    private Integer getOrderedAmounts(Article article, Calendar begin, Calendar end, CronjobController controller) {
        return controller.customerOrderRepository.countOrdersOfArticleInTimespan(
                article.id,
                new Timestamp(begin.getTimeInMillis()), new Timestamp(end.getTimeInMillis()));
    }

    /**
     * Calculates the amount of ordered articles by customers for the given article type in the three days before the
     * given reference time. The return-array contains 3 fields: Index 0: Orders 72 to 48 hours ago; Index 1: Orders 48
     * to 24 hours ago; Index 2: Orders 24 to 0 hours ago.
     *
     * @param article The article for which the customer orders are checked.
     * @param time    The reference time to use for calculation of the last orders.
     * @return A 3-element array containing the orders of the last three days.
     */
    private Integer[] getOrderedAmounts(Article article, Calendar time, CronjobController controller) {
        Calendar oneDayBefore = (Calendar) time.clone();
        oneDayBefore.add(Calendar.DAY_OF_MONTH, -1);
        Calendar twoDaysBefore = (Calendar) time.clone();
        twoDaysBefore.add(Calendar.DAY_OF_MONTH, -2);
        Calendar threeDaysBefore = (Calendar) time.clone();
        threeDaysBefore.add(Calendar.DAY_OF_MONTH, -3);

        return new Integer[]{ //
                getOrderedAmounts(article, threeDaysBefore, twoDaysBefore, controller), //
                getOrderedAmounts(article, twoDaysBefore, oneDayBefore, controller), //
                getOrderedAmounts(article, oneDayBefore, time, controller), //
        };
    }

    private HashMap<ReadSupplierDataAction.ArticleIdentifier, ArticleOffer> mapArticleOffers(List<ArticleOffer> articleOffers) {
        HashMap<ReadSupplierDataAction.ArticleIdentifier, ArticleOffer> map = new HashMap<>();
        for (ArticleOffer articleOffer : articleOffers) {
            ReadSupplierDataAction.ArticleIdentifier identifier = new ReadSupplierDataAction.ArticleIdentifier(articleOffer.manufacturer, articleOffer.articleNumber);
            map.put(identifier, articleOffer);
        }
        return map;
    }

    @Override
    public void executeAt(Calendar time, CronjobController controller) {
        List<Supplier> suppliers = controller.supplierRepository.findAll();
        ReadSupplierDataAction.Result supplierData = new ReadSupplierDataAction(suppliers).finish();

        // Save the new offers in the database
        List<ArticleOffer> allOffers = controller.articleOfferRepository.findAll();
        allOffers = new UpdateOffersAction(allOffers, supplierData.cheapestOffer).finish();
        controller.articleOfferRepository.saveAll(allOffers);

        HashMap<ReadSupplierDataAction.ArticleIdentifier, ArticleOffer> mappedOffers = mapArticleOffers(allOffers);

        // Reorder
        List<Article> allArticles = controller.articleRepository.findAll();
        for (Article article : allArticles) {
            Integer[] orderedAmounts = getOrderedAmounts(article, time, controller);

            Integer undeliveredReorders = controller.supplierOrderRepository
                    .countUndeliveredReorders(article.related.articleNumber);

            int amountInStock = controller.warehouseBookingPositionSlotEntryRepository
                    .getByArticle(article.id)
                    .stream()
                    .mapToInt(e -> e.newSumSlot)
                    .sum();

            ReorderAction action = new ReorderAction(article, orderedAmounts,
                    undeliveredReorders,
                    amountInStock,
                    supplierData.cheapestOffer, mappedOffers);
            SupplierOrder order = action.finish();
            if (order != null) {
                controller.supplierOrderRepository.save(order);

                // Create bookings for this order
                int netPrice = order.totalPriceNet;
                int vatPercent = order.ordered.vatPercent;
                int vatAmount = netPrice * vatPercent / 100;
                int grossPrice = netPrice + vatAmount;

                // Obligation towards the supplier
                BookingAccountEntry mainAccount = controller.bookingAccountEntryRepository.getByMain()
                        .orElseGet(BookingAccountEntry::newMain);
                BookingAccountEntry supplierAccount = controller.bookingAccountEntryRepository
                        .getBySupplier(order.supplier.id)
                        .orElseGet(() -> BookingAccountEntry.newSupplier(order.supplier));
                BookingReason obligationReason = new BookingReason(order);
                Booking obligationBooking = new CreateBookingAction(mainAccount,
                        supplierAccount,
                        obligationReason,
                        grossPrice).finish();
                controller.bookingRepository.save(obligationBooking);

                // Input Tax
                BookingAccountEntry vatAccount = controller.bookingAccountEntryRepository.getByVat()
                        .orElseGet(BookingAccountEntry::newVat);
                mainAccount = controller.bookingAccountEntryRepository.getByMain().get();
                BookingReason inputTaxReason = new BookingReason(order);
                Booking inputTaxBooking = new CreateBookingAction(vatAccount, mainAccount, inputTaxReason, vatAmount)
                        .finish();
                controller.bookingRepository.save(inputTaxBooking);
            }
        }
    }
}
