package de.accso.flexinale.besucherportal.api_in.web;

import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO;
import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO;
import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO;
import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache;
import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalStateException;
import de.accso.flexinale.ticketing.api_contract.event.model.TicketTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;

import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull;

class TicketSorter {

    private static final Logger LOGGER = LoggerFactory.getLogger(TicketSorter.class);

    /**
     * return a list of list of tickets, sorted by time (inner list) and date (outer list).
     * each list contains the tickets for a day
     */
    public static List<List<VorfuehrungMitAnzahlTicketsTO>> sortAndMapTicketsPerDay(final List<TicketTO> allTickets,
                                                                                    final FKKsVTInMemoryCache cache) {
        if (allTickets == null || allTickets.isEmpty()) {
            return new ArrayList<>();
        }

        Map<TicketTO, VorfuehrungTO> ticket2Vorfuehrung = createTicketToVorfuehrungMap(allTickets, cache);

        // 0) sort tickets by time (not done in database SQL query as before)
        allTickets.sort(new TicketTOByZeitSortingComparator(ticket2Vorfuehrung));

        // 1) tickets by day
        List<List<TicketTO>> ticketsProTag = new ArrayList<>();

        LocalDate previousDate = LocalDate.MIN; // far past :-)
        for (TicketTO ticket : allTickets) {
            LocalDateTime zeit = getRawOrNull(ticket2Vorfuehrung.get(ticket).zeit());
            LocalDate currentDate = zeit.toLocalDate();

            if (currentDate.equals(previousDate)) {
                ticketsProTag.getLast().add(ticket);
            }
            else {
                List<TicketTO> tickets = new ArrayList<>();
                tickets.add(ticket);
                ticketsProTag.add(tickets);
            }
            previousDate = currentDate;
        }

        // 2) inner structure: Vorfuehrung and tickets
        List<List<VorfuehrungMitAnzahlTicketsTO>> vorfuehrungUndAnzahlTicketsFuerVorfuehrungProTag = new ArrayList<>();

        for (List<TicketTO> ticketsForOneDay : ticketsProTag) {
            List<VorfuehrungMitAnzahlTicketsTO> vorfuehrungenUndAnzahlTicketsFuerEinenTag = new ArrayList<>();
            int anzahlGueltigeTicketsFuerVorfuehrung = 0;
            int anzahlUngueltigeTicketsFuerVorfuehrung = 0;
            VorfuehrungTO previousVorfuehrung = ticket2Vorfuehrung.get(ticketsForOneDay.getFirst());

            for (TicketTO ticket : ticketsForOneDay) {
                // inside each day, Vorfuehrungen are ordered by zeit.
                // There is never more than one Vorfuehrung at a zeit for a user.
                // So tickets for the same Vorfuehrung are all in a row - that's why the following works.
                VorfuehrungTO currentVorfuehrung = ticket2Vorfuehrung.get(ticket);

                if (currentVorfuehrung.equals(previousVorfuehrung)) {
                    Boolean ticketGueltig = getRawOrNull(ticket.gueltig());

                    if (ticketGueltig) {
                        anzahlGueltigeTicketsFuerVorfuehrung++;
                    }
                    else {
                        anzahlUngueltigeTicketsFuerVorfuehrung++;
                    }
                }
                else {
                    Boolean ticketGueltig = getRawOrNull(ticket.gueltig());

                    // add the information for previous Vorfuehrung
                    VorfuehrungMitAnzahlTicketsTO to = mapVorfuehrungToTO(
                            anzahlGueltigeTicketsFuerVorfuehrung, anzahlUngueltigeTicketsFuerVorfuehrung,
                            previousVorfuehrung, cache);
                    vorfuehrungenUndAnzahlTicketsFuerEinenTag.add(to);

                    // ... we also have a new ticket for the next Vorfuehrung
                    if (ticketGueltig) {
                        anzahlGueltigeTicketsFuerVorfuehrung = 1;
                        anzahlUngueltigeTicketsFuerVorfuehrung = 0;
                    } else {
                        anzahlGueltigeTicketsFuerVorfuehrung = 0;
                        anzahlUngueltigeTicketsFuerVorfuehrung = 1;
                    }
                }

                previousVorfuehrung = currentVorfuehrung;
            }

            // Also add the last Vorfuehrung an anzahlTickets when day is over
            VorfuehrungMitAnzahlTicketsTO to = mapVorfuehrungToTO(
                    anzahlGueltigeTicketsFuerVorfuehrung, anzahlUngueltigeTicketsFuerVorfuehrung, previousVorfuehrung, cache);
            vorfuehrungenUndAnzahlTicketsFuerEinenTag.add(to);
            vorfuehrungUndAnzahlTicketsFuerVorfuehrungProTag.add(vorfuehrungenUndAnzahlTicketsFuerEinenTag);
        }
        return vorfuehrungUndAnzahlTicketsFuerVorfuehrungProTag;
    }

    private static VorfuehrungMitAnzahlTicketsTO mapVorfuehrungToTO(final int anzahlGueltigeTicketsFuerVorfuehrung,
                                                                    final int anzahlUngueltigeTicketsFuerVorfuehrung,
                                                                    final VorfuehrungTO vorfuehrung,
                                                                    final FKKsVTInMemoryCache cache) {
        FilmTO film = vorfuehrung.film();
        KinoTO kino = cache.kino(vorfuehrung.kinoId());
        if (film == null) {
            throw new FlexinaleIllegalStateException("no Film for Vorfuehrung %s found".formatted(vorfuehrung.id()));
        }
        else {
            return new VorfuehrungMitAnzahlTicketsTO(
                    new VorfuehrungMitAnzahlTicketsTO.Zeit(getRawOrNull(vorfuehrung.zeit())),
                    film,
                    vorfuehrung.kinoSaal(),
                    kino,
                    new VorfuehrungMitAnzahlTicketsTO.AnzahlTickets(anzahlGueltigeTicketsFuerVorfuehrung),
                    new VorfuehrungMitAnzahlTicketsTO.AnzahlTickets(anzahlUngueltigeTicketsFuerVorfuehrung)
            );
        }
    }

    private static Map<TicketTO, VorfuehrungTO> createTicketToVorfuehrungMap(final List<TicketTO> allTickets,
                                                                             final FKKsVTInMemoryCache cache) {
        Map<TicketTO, VorfuehrungTO> ticketToVorfuehrung = new HashMap<>();
        for (TicketTO ticket : allTickets) {
           VorfuehrungTO vorfuehrung = cache.vorfuehrung(ticket.vorfuehrungId());
            if (vorfuehrung == null) {
                LOGGER.error("Vorfuehrung %s  of ticket %s cannot be found.".formatted(ticket.vorfuehrungId(), ticket.id()));
            }
            else {
                ticketToVorfuehrung.put(ticket, vorfuehrung);
            }
        }
        return ticketToVorfuehrung;
    }
}

class TicketTOByZeitSortingComparator implements Comparator<TicketTO>, Serializable {

    private final Map<TicketTO, VorfuehrungTO> ticket2Vorfuehrung;

    TicketTOByZeitSortingComparator(final Map<TicketTO, VorfuehrungTO> ticket2Vorfuehrung) {
        this.ticket2Vorfuehrung = ticket2Vorfuehrung;
    }

    @Override
    public int compare(final TicketTO t1, final TicketTO t2) {
        VorfuehrungTO v1 = ticket2Vorfuehrung.get(t1);
        VorfuehrungTO v2 = ticket2Vorfuehrung.get(t2);
        LocalDateTime v1Zeit = getRawOrNull(v1.zeit());
        LocalDateTime v2Zeit = getRawOrNull(v2.zeit());
        return v1Zeit.compareTo(v2Zeit);
    }
}