package de.accso.flexinale.ticketing.application.services;

import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalArgumentException;
import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalStateException;
import de.accso.flexinale.common.shared_kernel.Identifiable;
import de.accso.flexinale.ticketing.domain.dao.KontingentDao;
import de.accso.flexinale.ticketing.domain.dao.TicketDao;
import de.accso.flexinale.ticketing.domain.model.Kontingent;
import de.accso.flexinale.ticketing.domain.model.Ticket;
import jakarta.transaction.Transactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@Transactional
public class TicketService {
    private static final Logger LOGGER = LoggerFactory.getLogger(TicketService.class);

    private final TicketDao ticketDao;

    private final KontingentDao kontingentDao;

    private final OnlineKontingentChangedPublication onlineKontingentChangedPublisher;

    private final TicketPublication ticketPublisher;

    private final int quoteOnline;

    public TicketService(final TicketDao ticketDao,
                         final KontingentDao kontingentDao,
                         final OnlineKontingentChangedPublication onlineKontingentChangedPublisher,
                         final TicketPublication ticketPublisher,
                         final int quoteOnline) {
        this.ticketDao = ticketDao;
        this.kontingentDao = kontingentDao;
        this.onlineKontingentChangedPublisher = onlineKontingentChangedPublisher;
        this.ticketPublisher = ticketPublisher;
        this.quoteOnline = quoteOnline;
    }

    public List<Ticket> findByBesucher(final Identifiable.Id besucherId) {
        return ticketDao.findByBesucher(besucherId);
    }

    public List<Ticket> tickets() {
        return ticketDao.findAll();
    }

    public void loeseGutscheineOnlineFuerVorfuehrungEin(final Identifiable.Id vorfuehrungId, final Identifiable.Id filmId,
                                                        final Identifiable.Id besucherId, final int anzahlGutscheine) {
        loeseGutscheineFuerVorfuehrungEin(vorfuehrungId, filmId, besucherId, anzahlGutscheine,
                Ticket.VerkaufsKanal.ONLINE);
    }

    @SuppressWarnings("SameParameterValue")
    private void loeseGutscheineFuerVorfuehrungEin(final Identifiable.Id vorfuehrungId, final Identifiable.Id filmId,
                                                   final Identifiable.Id besucherId, final int anzahlGutscheine,
                                                   final Ticket.VerkaufsKanal verkaufsKanal) {
        Optional<Kontingent> optionalKontingent = kontingentDao.findByVorfuehrung(vorfuehrungId);

        if (optionalKontingent.isEmpty()) {
           String message = "Kontingent for Vorfuehrung %s could not be found".formatted(vorfuehrungId);
           LOGGER.error(message);
           throw new FlexinaleIllegalArgumentException(message);
        }

        Kontingent kontingent = optionalKontingent.get();
        Kontingent reduziertesKontingent = kontingent.reduziereKontingent(verkaufsKanal, anzahlGutscheine);
        Kontingent umAnzahlGekaufteTicketsReduziertesKontingent = kontingentDao.save(reduziertesKontingent);

        List<Ticket> ticketsToBePublished = new ArrayList<>();
        for (int ticketCounter=0; ticketCounter < anzahlGutscheine; ticketCounter++) {
            Ticket ticket = ticketDao.save(
                    new Ticket(Identifiable.Id.of(), filmId, vorfuehrungId, besucherId, verkaufsKanal));
            ticketsToBePublished.add(ticket);
        }
        ticketPublisher.publishGekaufteTickets(ticketsToBePublished);

        LOGGER.info("%d tickets bought for Vorfuehrung with id=%s and Besucher with id=%s on channel=%s"
                .formatted(anzahlGutscheine, vorfuehrungId, besucherId, verkaufsKanal));

        onlineKontingentChangedPublisher.publishOnlineKontingentChanged(List.of(umAnzahlGekaufteTicketsReduziertesKontingent));
    }

    public void gebeVorfuehrungInDenVerkauf(final Identifiable.Id vorfuehrungId, final int kapazitaet) {
        if (vorfuehrungIstBereitsImVerkauf(vorfuehrungId)) {
           String message = "Kontingent for Vorfuehrung %s already exists. No Kontingent created".formatted(vorfuehrungId);
           LOGGER.error(message);
           throw new FlexinaleIllegalStateException(message);
        }

        Kontingent kontingent = new Kontingent(Identifiable.Id.of(), kapazitaet, quoteOnline, vorfuehrungId);
        kontingentDao.save(kontingent);

        onlineKontingentChangedPublisher.publishOnlineKontingentChanged(List.of(kontingent));
    }

    public boolean vorfuehrungIstBereitsImVerkauf(final Identifiable.Id vorfuehrungId) {
        return kontingentDao.findByVorfuehrung(vorfuehrungId).isPresent();
    }

    public void aktualisiereVorfuehrung(final Identifiable.Id vorfuehrungId, final int kapazitaet){
        // 1) Invalidate all existing tickets
        List<Ticket> ticketsByVorfuehrung = ticketDao.findByVorfuehrung(vorfuehrungId);
        List<Ticket> ungueltigeTickets = new ArrayList<>();
        for (Ticket ticket : ticketsByVorfuehrung) {
            ticket.gueltig = new Ticket.Gueltig(false);
            Ticket ungueltigesTicket = ticketDao.save(ticket);
            ungueltigeTickets.add(ungueltigesTicket);
        }

        // 2) Recalculate Kontingent
        Optional<Kontingent> kontingentOptional = kontingentDao.findByVorfuehrung(vorfuehrungId);
        if (kontingentOptional.isEmpty()) {
            LOGGER.warn("could not recalculate Kontingente for Vorfuehrung %s. Kontingent does not exist. Creating new Kontingent"
                    .formatted(vorfuehrungId));
        }
        else {
            Kontingent kontingent = kontingentOptional.get();
            Kontingent neuBerechnetesKontingent = kontingentDao.save(
                    new Kontingent(kontingent.id, kontingent.version, kapazitaet, quoteOnline, kontingent.getVorfuehrungId())
            );

            // publish tickets and new Kontingent - at the end, after all database changes have succeeded.
            onlineKontingentChangedPublisher.publishOnlineKontingentChanged(List.of(neuBerechnetesKontingent));

            ticketPublisher.publishUngueltigeTickets(ungueltigeTickets);
        }
    }
}
