package fr.titionfire.ffsaf.domain.service;

import fr.titionfire.ffsaf.data.model.*;
import fr.titionfire.ffsaf.data.repository.*;
import fr.titionfire.ffsaf.net2.ServerCustom;
import fr.titionfire.ffsaf.net2.data.SimpleCompet;
import fr.titionfire.ffsaf.net2.request.SReqCompet;
import fr.titionfire.ffsaf.net2.request.SReqRegister;
import fr.titionfire.ffsaf.rest.client.dto.NotificationData;
import fr.titionfire.ffsaf.rest.data.*;
import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
import fr.titionfire.ffsaf.utils.*;
import fr.titionfire.ffsaf.ws.send.SRegister;
import io.quarkus.cache.Cache;
import io.quarkus.cache.CacheName;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.mailer.Mail;
import io.quarkus.mailer.reactive.ReactiveMailer;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import io.vertx.mutiny.core.Vertx;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.Response;
import org.hibernate.reactive.mutiny.Mutiny;
import org.jboss.logging.Logger;
import org.keycloak.representations.idm.UserRepresentation;

import java.util.*;
import java.util.stream.Stream;

@WithSession
@ApplicationScoped
public class CompetitionService {
    private static final Logger LOGGER = Logger.getLogger(CompetitionService.class);

    @Inject
    CompetitionRepository repository;

    @Inject
    CategoryRepository categoryRepository;

    @Inject
    MatchRepository matchRepository;

    @Inject
    RegisterRepository registerRepository;

    @Inject
    KeycloakService keycloakService;

    @Inject
    CombRepository combRepository;

    @Inject
    CompetitionGuestRepository competitionGuestRepository;

    @Inject
    CatPresetRepository catPresetRepository;

    @Inject
    ServerCustom serverCustom;

    @Inject
    MembreService membreService;

    @Inject
    CompetPermService permService;

    @Inject
    HelloAssoRegisterRepository helloAssoRepository;

    @SuppressWarnings("CdiInjectionPointsInspection")
    @Inject
    ReactiveMailer reactiveMailer;

    @Inject
    Vertx vertx;

    @Inject
    SRegister sRegister;

    @Inject
    @CacheName("safca-config")
    Cache cache;

    @Inject
    @CacheName("safca-have-access")
    Cache cacheAccess;

    @Inject
    @CacheName("have-access")
    Cache cacheNoneAccess;

    @Inject
    TradService trad;

    public Uni<CompetitionData> getById(SecurityCtx securityCtx, Long id) {
        return permService.hasViewPerm(securityCtx, id).map(cm -> {
            CompetitionData out = CompetitionData.fromModelLight(cm);
            if (cm.getAdmin() != null) {
                out.setCanEditRegisters(cm.getAdmin().stream().anyMatch(u -> u.equals(securityCtx.getSubject())));
            }
            return out;
        });
    }

    public Uni<CompetitionData> getByIdAdmin(SecurityCtx securityCtx, Long id) {
        if (id == 0) {
            return Uni.createFrom().item(new CompetitionData());
        }
        return permService.hasAdminViewPerm(securityCtx, id)
                .call(competitionModel -> Mutiny.fetch(competitionModel.getCatPreset()))
                .chain(competitionModel -> Mutiny.fetch(competitionModel.getInsc())
                        .chain(insc -> Mutiny.fetch(competitionModel.getGuests())
                                .map(guest -> CompetitionData.fromModel(competitionModel).addInsc(insc, guest)
                                        .addPresets(competitionModel.getCatPreset()))))
                .chain(data ->
                        vertx.getOrCreateContext().executeBlocking(() -> {
                            keycloakService.getUser(UUID.fromString(data.getOwner()))
                                    .ifPresent(user -> data.setOwner(user.getUsername()));
                            return data;
                        })
                );
    }

    public Uni<List<CompetitionData>> getAll(SecurityCtx securityCtx) {
        List<CompetitionData> out = new ArrayList<>();
        return permService.getAllHaveAdminAccess(securityCtx)
                .call(ids -> repository.list("id IN ?1", ids)
                        .invoke(cm -> {
                            out.addAll(cm.stream().map(CompetitionData::fromModelLight).toList());
                            out.forEach(competition -> competition.setCanEdit(true));
                        }))
                .chain(ids -> repository.list("id NOT IN ?1 AND ?2 IN admin", ids, securityCtx.getSubject())
                        .map(cm -> {
                            out.addAll(cm.stream().map(CompetitionData::fromModelLight).toList());
                            out.forEach(competition -> competition.setCanEditRegisters(true));
                            List<Long> ids2 = new ArrayList<>(ids);
                            ids2.addAll(cm.stream().map(CompetitionModel::getId).toList());
                            return ids2;
                        }))
                .call(ids ->
                        repository.list("id NOT IN ?1 AND (publicVisible = TRUE OR registerMode IN ?2)", ids,
                                        securityCtx.isClubAdmin() ? List.of(RegisterMode.FREE, RegisterMode.HELLOASSO,
                                                RegisterMode.CLUB_ADMIN) : List.of(RegisterMode.FREE, RegisterMode.HELLOASSO))
                                .invoke(cm -> out.addAll(cm.stream().map(CompetitionData::fromModelLight).toList()))
                                .call(cm -> registerRepository.list(
                                                "membre.userId = ?1 AND competition.id NOT IN ?2 AND competition NOT IN ?3",
                                                securityCtx.getSubject(), ids, cm)
                                        .chain(registerModels -> {
                                            Uni<Void> uni = Uni.createFrom().nullItem();
                                            for (RegisterModel registerModel : registerModels) {
                                                uni = uni.call(__ -> Mutiny.fetch(registerModel.getCompetition())
                                                        .invoke(cm2 -> out.add(CompetitionData.fromModelLight(cm2))));
                                            }
                                            return uni;
                                        })
                                ))
                .map(__ -> out);
    }

    public Uni<List<CompetitionData>> getAllAdmin(SecurityCtx securityCtx) {
        return permService.getAllHaveAdminAccess(securityCtx)
                .chain(ids -> repository.list("id IN ?1", ids))
                .map(pouleModels -> pouleModels.stream().map(CompetitionData::fromModel).toList());
    }

    public Uni<List<CompetitionData>> getAllSystemAdmin(SecurityCtx securityCtx,
                                                        CompetitionSystem system) {
        return permService.getAllHaveAdminAccess(securityCtx)
                .chain(ids -> repository.list("system = ?1 AND id IN ?2", system, ids))
                .map(pouleModels -> pouleModels.stream().map(CompetitionData::fromModel).toList());
    }

    public Uni<List<CompetitionData>> getAllSystemTable(SecurityCtx securityCtx,
                                                        CompetitionSystem system) {
        return repository.list("system = ?1", system)
                .chain(l -> Uni.join().all(l.stream().map(cm ->
                        permService.hasTablePerm(securityCtx, cm).onFailure().recoverWithNull()
                ).toList()).andCollectFailures())
                .map(l -> l.stream().filter(Objects::nonNull).map(CompetitionData::fromModel).toList());
    }

    public Uni<CompetitionData> addOrUpdate(SecurityCtx securityCtx, CompetitionData data) {
        if (data.getId() == null) {
            return combRepository.find("userId = ?1", securityCtx.getSubject()).firstResult()
                    .invoke(Unchecked.consumer(combModel -> {
                        if (!securityCtx.getRoles().contains("create_compet") && !securityCtx.getRoles()
                                .contains("federation_admin"))
                            throw new DForbiddenException(trad.t("vous.ne.pouvez.pas.creer.de.competition"));
                    }))
                    .map(MembreModel::getClub)
                    .chain(clubModel -> {
                        CompetitionModel model = new CompetitionModel();

                        model.setId(null);
                        model.setSystem(data.getSystem());
                        model.setClub(clubModel);
                        model.setInsc(new ArrayList<>());
                        model.setGuests(new ArrayList<>());
                        model.setUuid(UUID.randomUUID().toString());
                        model.setOwner(securityCtx.getSubject());
                        model.setCatPreset(new ArrayList<>());

                        copyData(data, model);

                        return Panache.withTransaction(() -> repository.persist(model));
                    })
                    .call(model -> syncPreset(data, model))
                    .map(CompetitionData::fromModel)
                    .call(c -> (c.getSystem() == CompetitionSystem.SAFCA) ? cacheAccess.invalidate(
                            securityCtx.getSubject()) : Uni.createFrom().nullItem())
                    .call(c -> (c.getSystem() == CompetitionSystem.INTERNAL) ? cacheNoneAccess.invalidate(
                            securityCtx.getSubject()) : Uni.createFrom().nullItem());
        } else {
            return permService.hasEditPerm(securityCtx, data.getId())
                    .call(model -> Mutiny.fetch(model.getCatPreset()))
                    .chain(model -> {
                        copyData(data, model);

                        return vertx.getOrCreateContext().executeBlocking(() -> // Update owner
                                        keycloakService.getUser(data.getOwner()).map(UserRepresentation::getId).orElse(null))
                                .invoke(Unchecked.consumer(newOwner -> {
                                    if (newOwner == null)
                                        throw new DBadRequestException(
                                                String.format(trad.t("user.not.found"), data.getOwner()));
                                    if (!newOwner.equals(model.getOwner())) {
                                        if (!securityCtx.roleHas("federation_admin")
                                                && !securityCtx.roleHas("safca_super_admin")
                                                && !securityCtx.getSubject().equals(model.getOwner()))
                                            throw new DForbiddenException();
                                        model.setOwner(newOwner);
                                    }
                                }))
                                .chain(__ -> Panache.withTransaction(() -> repository.persist(model)));
                    })
                    .call(model -> syncPreset(data, model))
                    .map(model -> CompetitionData.fromModel(model).addPresets(model.getCatPreset()))
                    .call(c -> (c.getSystem() == CompetitionSystem.SAFCA) ? cacheAccess.invalidate(
                            securityCtx.getSubject()) : Uni.createFrom().nullItem())
                    .call(c -> (c.getSystem() == CompetitionSystem.INTERNAL) ? cacheNoneAccess.invalidate(
                            securityCtx.getSubject()) : Uni.createFrom().nullItem());
        }
    }

    private Uni<?> syncPreset(CompetitionData data, CompetitionModel model) {
        List<Long> toRemoveId = model.getCatPreset().stream()
                .map(CatPresetModel::getId)
                .filter(id -> data.getPresets().stream().noneMatch(preset -> Objects.equals(preset.getId(), id)))
                .toList();

        for (PresetData preset : data.getPresets()) {
            CatPresetModel presetModel;
            if (preset.getId() != null && preset.getId() > 0) {
                presetModel = model.getCatPreset().stream()
                        .filter(p -> p.getId().equals(preset.getId()))
                        .findFirst()
                        .orElse(new CatPresetModel());
            } else {
                presetModel = new CatPresetModel();
                model.getCatPreset().add(presetModel);
            }

            presetModel.setCompetition(model);
            presetModel.setName(preset.getName());
            presetModel.setSwordType(preset.getSword());
            presetModel.setShieldType(preset.getShield());
            presetModel.setCategories(preset.getCategories());
            presetModel.setMandatoryProtection1(preset.getMandatoryProtection1());
            presetModel.setMandatoryProtection2(preset.getMandatoryProtection2());
        }

        // Remove deleted presets
        model.getCatPreset().removeIf(presetModel -> toRemoveId.contains(presetModel.getId()));

        return Panache.withTransaction(() -> repository.persist(model)
                .call(__ -> {
                    if (!toRemoveId.isEmpty()) {
                        return catPresetRepository.delete("id IN ?1", toRemoveId);
                    }
                    return Uni.createFrom().nullItem();
                }));
    }

    private void copyData(CompetitionData data, CompetitionModel model) {
        if (model.getBanMembre() == null)
            model.setBanMembre(new ArrayList<>());

        model.setName(data.getName());
        model.setAdresse(data.getAdresse());
        model.setDescription(data.getDescription());
        model.setDate(data.getDate());
        model.setTodate(data.getDate());
        model.setPublicVisible(data.isPublicVisible());
        model.setStartRegister(data.getStartRegister());
        model.setEndRegister(data.getEndRegister());
        model.setRegisterMode(data.getRegisterMode());
        model.setRequiredWeight(data.getRequiredWeight());
        model.setData1(data.getData1());
        model.setData2(data.getData2());
        model.setData3(data.getData3());
        model.setData4(data.getData4());
    }

    public Uni<List<SimpleRegisterComb>> getRegister(SecurityCtx securityCtx, Long id, String source) {
        if ("admin".equals(source))
            return permService.hasEditPerm(securityCtx, id)
                    .chain(c -> {
                        Uni<List<SimpleRegisterComb>> uni = Mutiny.fetch(c.getInsc())
                                .onItem().transformToMulti(Multi.createFrom()::iterable)
                                .onItem().call(combModel -> Mutiny.fetch(combModel.getMembre().getLicences()))
                                .onItem().call(combModel -> Mutiny.fetch(combModel.getCategoriesInscrites()))
                                .map(cm -> SimpleRegisterComb.fromModel(cm, cm.getMembre().getLicences())
                                        .setCategorieInscrite(cm.getCategoriesInscrites()))
                                .collect().asList();
                        return uni
                                .call(l -> Mutiny.fetch(c.getGuests())
                                        .onItem().transformToMulti(Multi.createFrom()::iterable)
                                        .filter(g -> !g.isTeam())
                                        .onItem().call(guest -> Mutiny.fetch(guest.getCategoriesInscrites()))
                                        .map(guest -> SimpleRegisterComb.fromModel(guest)
                                                .setCategorieInscrite(guest.getCategoriesInscrites()))
                                        .collect().asList()
                                        .invoke(l::addAll));
                    });

        if ("club".equals(source))
            return Uni.createFrom().nullItem()
                    .invoke(Unchecked.consumer(__ -> {
                        if (!securityCtx.isClubAdmin())
                            throw new DForbiddenException();
                    }))
                    .chain(__ -> membreService.getByAccountId(securityCtx.getSubject()))
                    .chain(model -> registerRepository.list("competition.id = ?1 AND membre.club = ?2", id,
                            model.getClub()))
                    .onItem().transformToMulti(Multi.createFrom()::iterable)
                    .onItem().call(combModel -> Mutiny.fetch(combModel.getMembre().getLicences()))
                    .onItem().call(combModel -> Mutiny.fetch(combModel.getCategoriesInscrites()))
                    .map(combModel -> SimpleRegisterComb.fromModel(combModel, combModel.getMembre().getLicences())
                            .setCategorieInscrite(combModel.getCategoriesInscrites()))
                    .collect().asList();

        return membreService.getByAccountId(securityCtx.getSubject())
                .chain(model -> registerRepository.find("competition.id = ?1 AND membre = ?2", id, model).firstResult()
                        .call(rm -> rm == null ? Uni.createFrom().voidItem() :
                                Mutiny.fetch(rm.getCategoriesInscrites()))
                        .map(rm -> rm == null ? List.of() : List.of(SimpleRegisterComb.fromModel(rm, List.of())
                                .setCategorieInscrite(rm.getCategoriesInscrites()))));
    }

    public Uni<SimpleRegisterComb> addRegisterComb(SecurityCtx securityCtx, Long id, RegisterRequestData data,
                                                   String source) {
        if ("admin".equals(source))
            if (data.getLicence() == null || data.getLicence() != -1) { // not a guest
                return permService.hasEditPerm(securityCtx, id)
                        .chain(c -> findComb(data.getLicence(), data.getFname(), data.getLname())
                                .call(combModel -> Mutiny.fetch(combModel.getLicences()))
                                .call(combModel -> {
                                    if (c.getBanMembre() == null)
                                        c.setBanMembre(new ArrayList<>());
                                    c.getBanMembre().remove(combModel.getId());
                                    return Panache.withTransaction(() -> repository.persist(c));
                                })
                                .chain(combModel -> updateRegister(data, c, combModel, true, false)))
                        .call(r -> r.getCompetition().getSystem() == CompetitionSystem.INTERNAL ?
                                sRegister.sendRegisterNoFetch(r.getCompetition().getUuid(), r) : Uni.createFrom()
                                .voidItem())
                        .map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences())
                                .setCategorieInscrite(r.getCategoriesInscrites()));
            } else {
                return permService.hasEditPerm(securityCtx, id)
                        .chain(c -> competitionGuestRepository.findById(data.getId() * -1)
                                .map(g -> {
                                    if (g != null)
                                        return g;
                                    CompetitionGuestModel model = new CompetitionGuestModel();
                                    model.setCompetition(c);
                                    return model;
                                }))
                        .invoke(model -> {
                            model.setFname(data.getFname());
                            if (data.getLname().equals("__team"))
                                model.setLname("_team");
                            else
                                model.setLname(data.getLname());
                            model.setGenre(data.getGenre());
                            model.setClub(data.getClub());
                            model.setCountry(data.getCountry());
                            model.setWeightReal(data.getWeightReal());
                            model.setCategorie(data.getCategorie());
                            if (model.getCompetition().getRequiredWeight().contains(model.getCategorie()))
                                model.setWeight(data.getWeight());
                        })
                        .call(g -> Mutiny.fetch(g.getCategoriesInscrites()))
                        .call(g -> catPresetRepository.list("competition = ?1 AND id IN ?2", g.getCompetition(),
                                        data.getCategoriesInscrites())
                                .invoke(cats -> {
                                    g.getCategoriesInscrites().clear();
                                    g.getCategoriesInscrites().addAll(cats);
                                    g.getCategoriesInscrites()
                                            .removeIf(cat -> cat.getCategories().stream()
                                                    .noneMatch(e -> e.getCategorie().equals(g.getCategorie())));
                                }))
                        .chain(model -> Panache.withTransaction(() -> competitionGuestRepository.persist(model))
                                .call(r -> model.getCompetition().getSystem() == CompetitionSystem.INTERNAL ?
                                        sRegister.sendRegisterNoFetch(model.getCompetition().getUuid(), r)
                                        : Uni.createFrom().voidItem()))
                        .map(g -> SimpleRegisterComb.fromModel(g).setCategorieInscrite(g.getCategoriesInscrites()));
            }
        if ("club".equals(source))
            return repository.findById(id)
                    .invoke(Unchecked.consumer(cm -> {
                        if (!(cm.getRegisterMode() == RegisterMode.CLUB_ADMIN || cm.getRegisterMode() == RegisterMode.FREE)
                                || !securityCtx.isClubAdmin())
                            throw new DForbiddenException();
                        if (new Date().before(cm.getStartRegister()) || new Date().after(cm.getEndRegister()))
                            throw new DBadRequestException(trad.t("inscription.fermee"));
                    }))
                    .chain(c -> findComb(data.getLicence(), data.getFname(), data.getLname())
                            .call(combModel -> Mutiny.fetch(combModel.getLicences()))
                            .invoke(Unchecked.consumer(model -> {
                                if (!securityCtx.isInClubGroup(model.getClub().getId()))
                                    throw new DForbiddenException();
                                if (c.getBanMembre().contains(model.getId()))
                                    throw new DForbiddenException(trad.t("insc.err1"));
                            }))
                            .chain(combModel -> updateRegister(data, c, combModel, false, false)))
                    .call(r -> r.getCompetition().getSystem() == CompetitionSystem.INTERNAL ?
                            sRegister.sendRegisterNoFetch(r.getCompetition().getUuid(), r) : Uni.createFrom()
                            .voidItem())
                    .map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences())
                            .setCategorieInscrite(r.getCategoriesInscrites()));

        return repository.findById(id)
                .invoke(Unchecked.consumer(cm -> {
                    if (cm.getRegisterMode() != RegisterMode.FREE)
                        throw new DForbiddenException();
                    if (new Date().before(cm.getStartRegister()) || new Date().after(cm.getEndRegister()))
                        throw new DBadRequestException(trad.t("inscription.fermee"));
                }))
                .chain(c -> membreService.getByAccountId(securityCtx.getSubject())
                        .invoke(Unchecked.consumer(model -> {
                            if (c.getBanMembre().contains(model.getId()))
                                throw new DForbiddenException(trad.t("insc.err2"));
                        }))
                        .chain(combModel -> updateRegister(data, c, combModel, false, false)))
                .call(r -> r.getCompetition().getSystem() == CompetitionSystem.INTERNAL ?
                        sRegister.sendRegisterNoFetch(r.getCompetition().getUuid(), r) : Uni.createFrom().voidItem())
                .map(r -> SimpleRegisterComb.fromModel(r, List.of()).setCategorieInscrite(r.getCategoriesInscrites()));
    }

    public Uni<List<SimpleRegisterComb>> addRegistersComb(SecurityCtx securityCtx, Long id,
                                                          List<RegisterRequestData> datas,
                                                          String source) {
        if (!"admin".equals(source))
            return Uni.createFrom().failure(new DForbiddenException());

        return permService.hasEditPerm(securityCtx, id)
                .chain(cm -> Multi.createFrom().iterable(datas).onItem().transformToUni(data ->
                        makeImportUpdate(cm, data).onFailure().recoverWithItem(t -> {
                            SimpleRegisterComb errorComb = new SimpleRegisterComb();
                            errorComb.setLicence(-42);
                            errorComb.setFname("ERROR");
                            errorComb.setLname(t.getMessage());
                            return errorComb;
                        })).concatenate().collect().asList());
    }

    @WithSession
    public Uni<SimpleRegisterComb> makeImportUpdate(CompetitionModel c, RegisterRequestData data) {
        if (data.getLicence() == null || data.getLicence() != -1) { // not a guest
            return findComb(data.getLicence(), data.getFname(), data.getLname())
                    .call(combModel -> Mutiny.fetch(combModel.getLicences()))
                    .call(combModel -> {
                        if (c.getBanMembre() == null)
                            c.setBanMembre(new ArrayList<>());
                        c.getBanMembre().remove(combModel.getId());
                        return Panache.withTransaction(() -> repository.persist(c));
                    })
                    .chain(combModel -> updateRegister(data, c, combModel, true, true))
                    .map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences())
                            .setCategorieInscrite(r.getCategoriesInscrites()));
        } else {
            return findGuestOrInit(data.getFname(), data.getLname(), c)
                    .invoke(Unchecked.consumer(model -> {
                        if (data.getCategorie() == null)
                            throw new DBadRequestException(trad.t("categorie.requise"));
                        model.setCategorie(data.getCategorie());

                        if (data.getGenre() == null) {
                            if (model.getGenre() == null)
                                data.setGenre(Genre.NA);
                        } else
                            model.setGenre(data.getGenre());

                        if (data.getClub() == null) {
                            if (model.getClub() == null)
                                data.setClub("");
                        } else
                            model.setClub(data.getClub());

                        if (data.getCountry() == null) {
                            if (model.getCountry() == null)
                                data.setCountry("FR");
                        } else
                            model.setCountry(data.getCountry());

                        if (c.getRequiredWeight().contains(model.getCategorie())) {
                            if (data.getCountry() != null)
                                model.setWeight(data.getWeight());
                        }
                    }))
                    .call(g -> Mutiny.fetch(g.getCategoriesInscrites()))
                    .call(g -> catPresetRepository.list("competition = ?1 AND id IN ?2", c,
                                    data.getCategoriesInscrites())
                            .invoke(cats -> {
                                g.getCategoriesInscrites().clear();
                                g.getCategoriesInscrites().addAll(cats);
                                g.getCategoriesInscrites().removeIf(cat -> cat.getCategories().stream()
                                        .noneMatch(e -> e.getCategorie().equals(g.getCategorie())));
                            }))
                    .chain(model -> Panache.withTransaction(() -> competitionGuestRepository.persist(model))
                            .call(r -> c.getSystem() == CompetitionSystem.INTERNAL ?
                                    sRegister.sendRegister(c.getUuid(), r) : Uni.createFrom().voidItem()))
                    .map(g -> SimpleRegisterComb.fromModel(g).setCategorieInscrite(g.getCategoriesInscrites()));
        }
    }

    private Uni<RegisterModel> updateRegister(RegisterRequestData data, CompetitionModel c,
                                              MembreModel combModel, boolean admin, boolean append) {
        return registerRepository.find("competition = ?1 AND membre = ?2", c, combModel).firstResult()
                .onFailure().recoverWithNull()
                .map(Unchecked.function(r -> {
                    if (r != null) {
                        if (!admin && r.isLockEdit())
                            throw new DForbiddenException(trad.t("insc.err3"));
                        if (data.getOverCategory() != null || !append)
                            if (data.getOverCategory() == null)
                                r.setOverCategory(0);
                            else
                                r.setOverCategory(data.getOverCategory());
                        r.setCategorie(
                                (combModel.getBirth_date() == null) ? combModel.getCategorie() :
                                        Utils.getCategoryFormBirthDate(combModel.getBirth_date(),
                                                c.getDate()));
                        int days = Utils.getDaysBeforeCompetition(c.getDate());
                        if (days > -7)
                            r.setClub(combModel.getClub());
                        if (c.getRequiredWeight().contains(r.getCategorie2()))
                            if (data.getCountry() != null || !append)
                                r.setWeight(data.getWeight());
                        if (admin) {
                            r.setWeightReal(data.getWeightReal());
                            r.setLockEdit(data.isLockEdit());
                        }
                    } else {
                        r = new RegisterModel(c, combModel, data.getWeight(), data.getOverCategory(),
                                (combModel.getBirth_date() == null) ? combModel.getCategorie() :
                                        Utils.getCategoryFormBirthDate(combModel.getBirth_date(),
                                                c.getDate()),
                                (combModel.getClub() == null) ? null : combModel.getClub());
                        if (admin) {
                            r.setWeightReal(data.getWeightReal());
                            r.setLockEdit(data.isLockEdit());
                        } else
                            r.setLockEdit(false);
                    }

                    if (c.getSystem() == CompetitionSystem.SAFCA) {
                        SReqRegister.sendIfNeed(serverCustom.clients,
                                new CompetitionData.SimpleRegister(r.getMembre().getId(),
                                        r.getOverCategory(), r.getWeight2(), r.getCategorie(),
                                        (r.getClub() == null) ? null : r.getClub().getId(),
                                        (r.getClub() == null) ? null : r.getClub().getName()), c.getId());
                    }
                    return r;
                }))
                .call(r -> Mutiny.fetch(r.getCategoriesInscrites()).chain(__ ->
                        catPresetRepository.list("competition = ?1 AND id IN ?2", c, data.getCategoriesInscrites())
                                .invoke(cats -> {
                                    if (data.isQuick()) {
                                        cats.removeIf(cat -> r.getCategoriesInscrites().stream()
                                                .anyMatch(cp -> cp.equals(cat)));
                                    } else {
                                        r.getCategoriesInscrites().clear();
                                    }
                                    r.getCategoriesInscrites().addAll(cats);
                                    r.getCategoriesInscrites()
                                            .removeIf(cat -> cat.getCategories().stream()
                                                    .noneMatch(e -> e.getCategorie().equals(r.getCategorie2())));
                                })))
                .chain(r -> Panache.withTransaction(() -> registerRepository.persist(r)))
                .call(r -> c.getSystem() == CompetitionSystem.INTERNAL ?
                        sRegister.sendRegister(c.getUuid(), r) : Uni.createFrom().voidItem());
    }

    private Uni<CompetitionGuestModel> findGuestOrInit(String fname, String lname, CompetitionModel competition) {
        if (fname == null || lname == null)
            return Uni.createFrom().failure(new DBadRequestException(trad.t("nom.et.prenom.requis")));
        return competitionGuestRepository.find(
                        "unaccent(lname) ILIKE unaccent(?1) AND unaccent(fname) ILIKE unaccent(?2) AND competition = ?3",
                        lname, fname, competition).firstResult()
                .map(guestModel -> {
                    if (guestModel == null) {
                        CompetitionGuestModel model = new CompetitionGuestModel();
                        model.setFname(fname);
                        if (lname.equals("__team"))
                            model.setLname("_team");
                        else
                            model.setLname(lname);
                        model.setCompetition(competition);
                        return model;
                    }
                    return guestModel;
                });
    }

    private Uni<MembreModel> findComb(Long licence, String fname, String lname) {
        if (licence != null && licence > 0) {
            return combRepository.find("licence = ?1", licence).firstResult()
                    .invoke(Unchecked.consumer(combModel -> {
                        if (combModel == null)
                            throw new DForbiddenException(String.format(trad.t("licence.non.trouve"), licence));
                    }));
        } else {
            if (fname == null || lname == null)
                return Uni.createFrom().failure(new DBadRequestException(trad.t("nom.et.prenom.requis")));
            return combRepository.find("unaccent(lname) ILIKE unaccent(?1) AND unaccent(fname) ILIKE unaccent(?2)",
                            lname,
                            fname).firstResult()
                    .invoke(Unchecked.consumer(combModel -> {
                        if (combModel == null)
                            throw new DForbiddenException(String.format(trad.t("combattant.non.trouve"), fname, lname));
                    }));
        }
    }

    public Uni<Void> removeRegisterComb(SecurityCtx securityCtx, Long id, Long combId, String source, boolean ban) {
        if ("admin".equals(source))
            return permService.hasEditPerm(securityCtx, id)
                    .chain(cm -> {
                        if (cm.getBanMembre() == null)
                            cm.setBanMembre(new ArrayList<>());
                        if (ban) {
                            if (!cm.getBanMembre().contains(combId))
                                cm.getBanMembre().add(combId);
                        } else {
                            cm.getBanMembre().remove(combId);
                        }
                        return Panache.withTransaction(() -> repository.persist(cm));
                    })
                    .chain(c -> deleteRegister(combId, c, true));
        if ("club".equals(source))
            return repository.findById(id)
                    .invoke(Unchecked.consumer(cm -> {
                        if (!(cm.getRegisterMode() == RegisterMode.CLUB_ADMIN || cm.getRegisterMode() == RegisterMode.FREE)
                                || !securityCtx.isClubAdmin())
                            throw new DForbiddenException();
                        if (new Date().before(cm.getStartRegister()) || new Date().after(cm.getEndRegister()))
                            throw new DBadRequestException(trad.t("inscription.fermee"));
                    }))
                    .call(cm -> membreService.getById(combId)
                            .invoke(Unchecked.consumer(model -> {
                                if (model == null)
                                    throw new DNotFoundException(
                                            String.format(trad.t("le.membre.n.existe.pas"), combId));
                                if (!securityCtx.isInClubGroup(model.getClub().getId()))
                                    throw new DForbiddenException();
                            })))
                    .chain(c -> deleteRegister(combId, c, false));

        return repository.findById(id)
                .call(cm -> membreService.getByAccountId(securityCtx.getSubject())
                        .invoke(Unchecked.consumer(model -> {
                            if (cm.getRegisterMode() != RegisterMode.FREE || !Objects.equals(model.getId(), combId))
                                throw new DForbiddenException();
                            if (new Date().before(cm.getStartRegister()) || new Date().after(cm.getEndRegister()))
                                throw new DBadRequestException(trad.t("inscription.fermee"));
                        })))
                .chain(c -> deleteRegister(combId, c, false));
    }

    private Uni<Void> deleteRegister(Long combId, CompetitionModel c, boolean admin) {
        if (admin && combId < 0) {
            return competitionGuestRepository.find("competition = ?1 AND id = ?2", c, combId * -1).firstResult()
                    .onFailure().transform(t -> new DBadRequestException(trad.t("combattant.non.inscrit")))
                    .call(Unchecked.function(
                            model -> Panache.withTransaction(() -> competitionGuestRepository.delete(model))
                                    .call(r -> c.getSystem() == CompetitionSystem.INTERNAL ?
                                            sRegister.sendRegisterRemove(c.getUuid(), combId) : Uni.createFrom()
                                            .voidItem())))
                    .replaceWithVoid();
        }
        return registerRepository.find("competition = ?1 AND membre.id = ?2", c, combId).firstResult()
                .onFailure().transform(t -> new DBadRequestException(trad.t("combattant.non.inscrit")))
                .call(Unchecked.function(registerModel -> {
                    if (!admin && registerModel.isLockEdit())
                        throw new DForbiddenException(trad.t("insc.err3"));
                    return Panache.withTransaction(() -> registerRepository.delete(registerModel))
                            .call(r -> c.getSystem() == CompetitionSystem.INTERNAL ?
                                    sRegister.sendRegisterRemove(c.getUuid(), combId) : Uni.createFrom().voidItem());
                }))
                .replaceWithVoid();
    }

    public Uni<?> delete(SecurityCtx securityCtx, Long id) {
        return repository.findById(id).invoke(Unchecked.consumer(c -> {
                    if (!(securityCtx.getSubject().equals(c.getOwner()) || securityCtx.roleHas("federation_admin")))
                        throw new DForbiddenException();
                }))
                .call(competitionModel -> categoryRepository.list("compet = ?1", competitionModel)
                        .call(pouleModels -> pouleModels.isEmpty() ? Uni.createFrom().nullItem() :
                                Uni.join().all(pouleModels.stream()
                                                .map(pouleModel -> Panache.withTransaction(
                                                        () -> matchRepository.delete("poule = ?1", pouleModel.getId())))
                                                .toList())
                                        .andCollectFailures()))
                .call(competitionModel -> Panache.withTransaction(
                        () -> categoryRepository.delete("compet = ?1", competitionModel)))
                .call(competitionModel -> Panache.withTransaction(
                        () -> registerRepository.delete("competition = ?1", competitionModel)))
                .call(competitionModel -> Panache.withTransaction(
                        () -> competitionGuestRepository.delete("competition = ?1", competitionModel)))
                .chain(model -> Panache.withTransaction(() -> repository.delete("id", model.getId())))
                .invoke(o -> SReqCompet.rmCompet(serverCustom.clients, id))
                .call(__ -> cache.invalidate(id));
    }

    public Uni<SimpleCompetData> getInternalData(SecurityCtx securityCtx, Long id) {
        return permService.hasEditPerm(securityCtx, id)
                .invoke(Unchecked.consumer(cm -> {
                    if (cm.getSystem() != CompetitionSystem.INTERNAL)
                        throw new DBadRequestException(trad.t("competition.is.not.internal"));
                }))
                .chain(competitionModel -> {
                    SimpleCompetData data = SimpleCompetData.fromModel(competitionModel);
                    return vertx.getOrCreateContext().executeBlocking(() -> {
                        if (competitionModel.getAdmin() != null)
                            data.setAdmin(
                                    competitionModel.getAdmin().stream().map(uuid -> keycloakService.getUserById(uuid))
                                            .filter(Optional::isPresent)
                                            .map(user -> user.get().getUsername())
                                            .toList());
                        if (competitionModel.getTable() != null)
                            data.setTable(
                                    competitionModel.getTable().stream().map(uuid -> keycloakService.getUserById(uuid))
                                            .filter(Optional::isPresent)
                                            .map(user -> user.get().getUsername())
                                            .toList());
                        return data;
                    });
                });
    }

    public Uni<Void> setInternalData(SecurityCtx securityCtx, SimpleCompetData data) {
        return permService.hasEditPerm(securityCtx, data.getId())
                .invoke(Unchecked.consumer(cm -> {
                    if (cm.getSystem() != CompetitionSystem.INTERNAL)
                        throw new DBadRequestException(trad.t("competition.is.not.internal"));
                }))
                .chain(cm -> vertx.getOrCreateContext().executeBlocking(() -> {
                    ArrayList<String> admin = new ArrayList<>();
                    ArrayList<String> table = new ArrayList<>();
                    for (String username : data.getAdmin()) {
                        Optional<UserRepresentation> opt = keycloakService.getUser(username);
                        if (opt.isEmpty())
                            throw new DBadRequestException(String.format(trad.t("user.not.found"), username));
                        admin.add(opt.get().getId());
                    }
                    for (String username : data.getTable()) {
                        Optional<UserRepresentation> opt = keycloakService.getUser(username);
                        if (opt.isEmpty())
                            throw new DBadRequestException(String.format(trad.t("user.not.found"), username));
                        table.add(opt.get().getId());
                    }

                    cm.setAdmin(admin);
                    cm.setTable(table);
                    cm.setConfig(data.getConfigForInternal());

                    return cm;
                }))
                .chain(cm -> Panache.withTransaction(() -> repository.persist(cm)))
                .replaceWithVoid();
    }

    public Uni<SimpleCompetData> getSafcaData(SecurityCtx securityCtx, Long id) {
        return permService.getSafcaConfig(id)
                .call(Unchecked.function(o -> {
                    if (!securityCtx.getSubject().equals(o.owner())
                            && !securityCtx.roleHas("federation_admin")
                            && !securityCtx.roleHas("safca_super_admin")
                            && !o.admin().contains(UUID.fromString(securityCtx.getSubject()))
                            && !o.table().contains(UUID.fromString(securityCtx.getSubject())))
                        throw new DForbiddenException();
                    return Uni.createFrom().nullItem();
                }))
                .chain(simpleCompet -> {
                    SimpleCompetData data = SimpleCompetData.fromModel(simpleCompet);
                    return vertx.getOrCreateContext().executeBlocking(() -> {
                        data.setAdmin(simpleCompet.admin().stream().map(uuid -> keycloakService.getUser(uuid))
                                .filter(Optional::isPresent)
                                .map(user -> user.get().getUsername())
                                .toList());
                        data.setTable(simpleCompet.table().stream().map(uuid -> keycloakService.getUser(uuid))
                                .filter(Optional::isPresent)
                                .map(user -> user.get().getUsername())
                                .toList());

                        return data;
                    });
                });
    }

    public Uni<?> setSafcaData(SecurityCtx securityCtx, SimpleCompetData data) {
        return permService.hasEditPerm(securityCtx, data.getId())
                .chain(__ -> vertx.getOrCreateContext().executeBlocking(() -> {
                    ArrayList<UUID> admin = new ArrayList<>();
                    ArrayList<UUID> table = new ArrayList<>();
                    for (String username : data.getAdmin()) {
                        Optional<UserRepresentation> opt = keycloakService.getUser(username);
                        if (opt.isEmpty())
                            throw new DBadRequestException(String.format(trad.t("user.not.found"), username));
                        admin.add(UUID.fromString(opt.get().getId()));
                    }
                    for (String username : data.getTable()) {
                        Optional<UserRepresentation> opt = keycloakService.getUser(username);
                        if (opt.isEmpty())
                            throw new DBadRequestException(String.format(trad.t("user.not.found"), username));
                        table.add(UUID.fromString(opt.get().getId()));
                    }

                    return new SimpleCompet(data.getId(), "", data.isShow_blason(),
                            data.isShow_flag(), admin, table);
                }))
                .invoke(simpleCompet -> SReqCompet.sendUpdate(serverCustom.clients, simpleCompet))
                .call(simpleCompet -> permService.getSafcaConfig(data.getId())
                        .call(c -> {
                            List<Uni<Void>> list = Stream.concat(
                                    Stream.concat(
                                            c.admin().stream().filter(uuid -> !simpleCompet.admin().contains(uuid)),
                                            simpleCompet.admin().stream().filter(uuid -> !c.admin().contains(uuid))),
                                    Stream.concat(
                                            c.table().stream().filter(uuid -> !simpleCompet.table().contains(uuid)),
                                            simpleCompet.table().stream().filter(uuid -> !c.table().contains(uuid)))
                            ).map(uuid -> cacheAccess.invalidate(uuid.toString())).toList();

                            if (list.isEmpty())
                                return Uni.createFrom().nullItem();
                            return Uni.join().all(list).andCollectFailures();
                        }))
                .call(__ -> cache.invalidate(data.getId()));
    }

    public Uni<List<PresetData>> getPresetsForCompetition(SecurityCtx securityCtx, Long id) {
        return permService.hasViewPerm(securityCtx, id)
                .chain(cm -> Mutiny.fetch(cm.getCatPreset()))
                .map(p -> p.stream().map(PresetData::fromModel).toList());
    }

    public Uni<Response> unregisterHelloAsso(NotificationData data) {
        if (!data.getState().equals("Refunded"))
            return Uni.createFrom().item(Response.ok().build());

        return helloAssoRepository.list("orderId = ?1", data.getOrder().getId())
                .chain(regs -> {
                    Uni<?> uni = Uni.createFrom().nullItem();

                    for (HelloAssoRegisterModel reg : regs) {
                        if (reg.getCompetition().getRegisterMode() != RegisterMode.HELLOASSO)
                            continue;
                        if (!data.getOrder().getOrganizationSlug().equalsIgnoreCase(reg.getCompetition().getData1()))
                            continue;

                        uni = uni.call(__ -> Panache.withTransaction(
                                        () -> registerRepository.delete("competition = ?1 AND membre = ?2",
                                                reg.getCompetition(), reg.getMembre())))
                                .call(r -> reg.getCompetition().getSystem() == CompetitionSystem.INTERNAL ?
                                        sRegister.sendRegisterRemove(reg.getCompetition().getUuid(),
                                                reg.getMembre().getId()) : Uni.createFrom().voidItem()).onFailure()
                                .recoverWithNull()
                        ;
                    }

                    return uni;
                })
                .onFailure().invoke(Throwable::printStackTrace)
                .map(__ -> Response.ok().build());
    }

    public Uni<Response> registerHelloAsso(NotificationData data) {
        String organizationSlug = data.getOrganizationSlug();
        String formSlug = data.getFormSlug();
        RegisterRequestData req = new RegisterRequestData(null, "", "", null, null, 0, false, new ArrayList<>(), null,
                Categorie.CADET, Genre.NA, null, "fr", false);

        return repository.find("data1 = ?1 AND data2 = ?2", organizationSlug, formSlug).firstResult()
                .onFailure().recoverWithNull()
                .chain(cm -> {
                    Uni<?> uni = Uni.createFrom().nullItem();
                    if (cm == null || cm.getRegisterMode() != RegisterMode.HELLOASSO)
                        return uni;

                    List<String> place = List.of(cm.getData3().toLowerCase().split(";"));
                    List<String> fail = new ArrayList<>();

                    for (NotificationData.Item item : data.getItems()) {
                        if (!place.contains(item.getName().toLowerCase()))
                            continue;
                        if (item.getCustomFields() == null || item.getCustomFields().isEmpty()) {
                            fail.add("%s %s - licence n°???".formatted(item.getUser().getLastName(),
                                    item.getUser().getFirstName()));
                            continue;
                        }

                        Optional<Long> optional = item.getCustomFields().stream()
                                .filter(cf -> cf.getName().equalsIgnoreCase("Numéro de licence")).findAny().map(
                                        NotificationData.CustomField::getAnswer).map(Long::valueOf);

                        if (optional.isPresent()) {
                            uni = uni.call(__ -> membreService.getByLicence(optional.get())
                                            .invoke(Unchecked.consumer(m -> {
                                                if (m == null)
                                                    throw new NotFoundException();
                                            }))
                                            .call(m -> Panache.withTransaction(() ->
                                                    helloAssoRepository.persist(
                                                            new HelloAssoRegisterModel(cm, m, data.getId()))))
                                            .chain(m -> updateRegister(req, cm, m, true, true)))
                                    .onFailure().recoverWithItem(throwable -> {
                                        fail.add("%s %s - licence n°%d".formatted(item.getUser().getLastName(),
                                                item.getUser().getFirstName(), optional.get()));
                                        return null;
                                    })
                                    .replaceWithVoid();
                        } else {
                            fail.add("%s %s - licence n°???".formatted(item.getUser().getLastName(),
                                    item.getUser().getFirstName()));
                        }
                    }

                    return uni.call(__ -> fail.isEmpty() ? Uni.createFrom().nullItem() :
                            reactiveMailer.send(
                                    Mail.withText(cm.getData4(),
                                            "FFSAF - Compétition - Erreur HelloAsso",
                                            String.format(
                                                    """
                                                            Bonjour,
                                                            
                                                            Une erreur a été rencontrée lors de l'enregistrement d'une inscription à votre compétition %s pour les combattants suivants:
                                                            %s
                                                            
                                                            Cordialement,
                                                            L'intranet de la FFSAF
                                                            """, cm.getName(), String.join("\r\n", fail))
                                    ).setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("support@ffsaf.fr")
                            ).onFailure().invoke(e -> LOGGER.error("Fail to send email", e)));
                })
                .onFailure().invoke(Throwable::printStackTrace)
                .map(__ -> Response.ok().build());
    }
}
