package fr.titionfire.ffsaf.domain.service;

import fr.titionfire.ffsaf.data.model.*;
import fr.titionfire.ffsaf.data.repository.*;
import fr.titionfire.ffsaf.domain.entity.MatchModelExtend;
import fr.titionfire.ffsaf.rest.data.ResultCategoryData;
import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.utils.*;
import io.quarkus.cache.Cache;
import io.quarkus.cache.CacheName;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.runtime.annotations.RegisterForReflection;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import lombok.Builder;
import lombok.Data;
import org.hibernate.reactive.mutiny.Mutiny;

import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;

@WithSession
@ApplicationScoped
public class ResultService {

    @Inject
    CompetitionRepository compRepository;

    @Inject
    RegisterRepository registerRepository;

    @Inject
    MembreService membreService;

    @Inject
    ClubRepository clubRepository;

    @Inject
    CompetitionGuestRepository competitionGuestRepository;

    @Inject
    CategoryRepository categoryRepository;

    @Inject
    MatchRepository matchRepository;

    @Inject
    CardRepository cardRepository;

    @Inject
    TradService trad;

    @Inject
    @CacheName("club-classement")
    Cache cacheClubClassement;

    private static final HashMap<Long, String> combTempIds = new HashMap<>();

    private static String getCombTempId(Long key) {
        synchronized (combTempIds) {
            if (!combTempIds.containsKey(key)) {
                combTempIds.put(key, UUID.randomUUID().toString());
            }
            return combTempIds.get(key);
        }
    }

    private static Long getCombTempId(String value) {
        synchronized (combTempIds) {
            for (Map.Entry<Long, String> entry : combTempIds.entrySet()) {
                if (entry.getValue().equals(value)) {
                    return entry.getKey();
                }
            }
            return null;
        }
    }

    private static final HashMap<String, Long> clubTempIds = new HashMap<>();

    private static Long getClubTempId(String key) {
        synchronized (clubTempIds) {
            if (!clubTempIds.containsKey(key)) {
                clubTempIds.put(key, (System.currentTimeMillis() + clubTempIds.size()) * -1);
            }
            return clubTempIds.get(key);
        }
    }

    private static String getClubTempId(Long value) {
        synchronized (clubTempIds) {
            for (Map.Entry<String, Long> entry : clubTempIds.entrySet()) {
                if (entry.getValue().equals(value)) {
                    return entry.getKey();
                }
            }
            return null;
        }
    }

    public Uni<List<Object[]>> getList(SecurityCtx securityCtx) {
        return membreService.getByAccountId(securityCtx.getSubject())
                .chain(m -> registerRepository.list(
                        "membre = ?1 OR (TRUE = ?2 AND membre.club = ?3)",
                        m, securityCtx.isClubAdmin(), m.getClub()))
                .onItem().transformToMulti(Multi.createFrom()::iterable)
                .onItem().call(r -> Mutiny.fetch(r.getCompetition()))
                .onItem().transform(RegisterModel::getCompetition)
                .collect().asList()
                .chain(l -> compRepository.list("owner = ?1 OR ?1 IN admin", securityCtx.getSubject())
                        .map(l2 -> Stream.concat(l.stream(), l2.stream()).distinct()
                                .map(c -> new Object[]{c.getUuid(), c.getName(), c.getDate()}).toList())
                );
    }

    public Uni<HashMap<String, Long>> getCategoryList(String uuid, SecurityCtx securityCtx) {
        return hasAccess(uuid, securityCtx).chain(__ -> getCategoryList(uuid));
    }

    public Uni<HashMap<String, Long>> getCategoryList(String uuid) {
        return categoryRepository.list("compet.uuid = ?1", uuid)
                .map(categoryModels -> {
                    HashMap<String, Long> map = new HashMap<>();
                    categoryModels.stream()
                            .sorted(Comparator.comparing(CategoryModel::getName))
                            .forEachOrdered(categoryModel -> map.put(categoryModel.getName(), categoryModel.getId()));
                    return map;
                });
    }

    public Uni<ResultCategoryData> getCategory(String uuid, long poule, SecurityCtx securityCtx) {
        return hasAccess(uuid, securityCtx).chain(membreModel -> getData(uuid, poule, membreModel));
    }

    public Uni<ResultCategoryData> getCategory(String uuid, long poule) {
        return getData(uuid, poule, null);
    }

    private Uni<ResultCategoryData> getData(String uuid, long poule, MembreModel membreModel) {
        List<CardModel> cards = new ArrayList<>();

        return matchRepository.list("category.compet.uuid = ?1 AND category.id = ?2", uuid, poule)
                .call(list -> list.isEmpty() ? Uni.createFrom().voidItem() :
                        Mutiny.fetch(list.get(0).getCategory().getTree()))
                .chain(list -> cardRepository.list("competition.uuid = ?1", uuid).invoke(cards::addAll)
                        .map(c -> list.stream().map(m -> new MatchModelExtend(m, c)).toList()))
                .map(matchModels -> {
                    ResultCategoryData out = new ResultCategoryData();

                    CategoryModel categoryModel = matchModels.get(0).getCategory();
                    out.setName(categoryModel.getName());
                    out.setType(categoryModel.getType());
                    out.setLiceName(categoryModel.getLiceName() == null ? new String[]{} : categoryModel.getLiceName()
                            .split(";"));
                    out.setGenTime(System.currentTimeMillis());
                    out.setTreeIsClassement(categoryModel.isTreeAreClassement());

                    getArray2(matchModels, membreModel, out);
                    getTree(categoryModel.getTree(), membreModel, cards, out);
                    getClassementArray(categoryModel, membreModel, cards, out);
                    return out;
                });
    }

    public void getArray2(List<MatchModelExtend> matchModels_, MembreModel membreModel, ResultCategoryData out) {
        List<MatchModelExtend> matchModels = matchModels_.stream().filter(o -> o.getCategory_ord() >= 0).toList();

        HashMap<Character, List<MatchModelExtend>> matchMap = new HashMap<>();
        for (MatchModelExtend model : matchModels) {
            char g = model.getPoule();
            if (!matchMap.containsKey(g))
                matchMap.put(g, new ArrayList<>());
            matchMap.get(g).add(model);
        }

        matchMap.forEach((c, matchEntities) -> {
            List<ResultCategoryData.PouleArrayData> matchs = matchEntities.stream()
                    .sorted(Comparator.comparing(MatchModelExtend::getCategory_ord))
                    .map(o -> ResultCategoryData.PouleArrayData.fromModel(o, membreModel,
                            ResultPrivacy.REGISTERED_ONLY_NO_DETAILS))
                    .toList();

            List<ResultCategoryData.RankArray> rankArray = matchEntities.stream()
                    .flatMap(m -> Stream.of(m.getC1_id(), m.getC2_id(), m.getC1_guest(), m.getC2_guest()))
                    .distinct()
                    .filter(Objects::nonNull)
                    .map(comb -> {
                        CombStat stat = makeStat(matchEntities, comb);
                        return new ResultCategoryData.RankArray(0, comb,
                                comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS), stat.score, stat.w,
                                stat.pointMake, stat.pointTake, stat.getPointRate());
                    })
                    .filter(r -> r.getPointMake() > 0 || r.getPointTake() > 0)
                    .sorted(Comparator
                            .comparing(ResultCategoryData.RankArray::getScore)
                            .thenComparing(ResultCategoryData.RankArray::getWin)
                            .thenComparing(ResultCategoryData.RankArray::getPointRate).reversed())
                    .toList();
            out.getMatchs().put(c, matchs);

            int lastScore = -1;
            int lastWin = -1;
            float pointRate = 0;
            int rank = 0;
            for (ResultCategoryData.RankArray rankArray1 : rankArray) {
                if (rankArray1.getScore() != lastScore || rankArray1.getWin() != lastWin || pointRate != rankArray1.getPointRate()) {
                    lastScore = rankArray1.getScore();
                    lastWin = rankArray1.getWin();
                    pointRate = rankArray1.getPointRate();
                    rank++;
                }
                rankArray1.setRank(rank);
            }
            out.getRankArray().put(c, rankArray);
        });
    }

    public void getClassementArray(CategoryModel categoryModel, MembreModel membreModel, List<CardModel> cards,
                                   ResultCategoryData out) {
        if ((categoryModel.getType() & 2) != 0) {
            AtomicInteger rank = new AtomicInteger(0);
            categoryModel.getTree().stream()
                    .filter(t -> t.getLevel() > 0)
                    .sorted(Comparator.comparing(TreeModel::getLevel))
                    .forEach(treeModel -> makeClassementRow(membreModel,
                            new MatchModelExtend(treeModel.getMatch(), cards), out, rank));

            categoryModel.getTree().stream()
                    .filter(t -> t.getLevel() <= -10)
                    .sorted(Comparator.comparing(TreeModel::getLevel).reversed())
                    .forEach(treeModel -> makeClassementRow(membreModel,
                            new MatchModelExtend(treeModel.getMatch(), cards), out, rank));
        } else {
            for (List<ResultCategoryData.RankArray> list : out.getRankArray().values()) {
                for (ResultCategoryData.RankArray r : list) {
                    out.getClassement()
                            .add(new ResultCategoryData.ClassementData(r.getRank(), r.getComb(), r.getName()));
                }
            }
        }
    }

    private static void makeClassementRow(MembreModel membreModel, MatchModelExtend m, ResultCategoryData out,
                                          AtomicInteger rank) {
        if (m.isEnd()) {
            if (m.getWin() > 0) {
                out.getClassement()
                        .add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), m.getC1(),
                                m.getC1Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS)));
                out.getClassement()
                        .add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), m.getC2(),
                                m.getC2Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS)));
            } else if (m.getWin() < 0) {
                out.getClassement()
                        .add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), m.getC2(),
                                m.getC2Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS)));
                out.getClassement()
                        .add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), m.getC1(),
                                m.getC1Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS)));
            } else {
                out.getClassement()
                        .add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), m.getC1(),
                                m.getC1Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS)));
                out.getClassement()
                        .add(new ResultCategoryData.ClassementData(rank.get(), m.getC2(),
                                m.getC2Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS)));
            }
        } else {
            out.getClassement().add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), null, null));
            out.getClassement().add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), null, null));
        }
    }

    private static void convertTree(TreeModel src, TreeNode<ResultCategoryData.TreeData> dst, MembreModel membreModel,
                                    ResultPrivacy privacy, List<CardModel> cards) {
        dst.setData(
                ResultCategoryData.TreeData.from(new MatchModelExtend(src.getMatch(), cards), src.getLevel(),
                        membreModel, privacy));
        if (src.getLeft() != null) {
            dst.setLeft(new TreeNode<>());
            convertTree(src.getLeft(), dst.getLeft(), membreModel, privacy, cards);
        }
        if (src.getRight() != null) {
            dst.setRight(new TreeNode<>());
            convertTree(src.getRight(), dst.getRight(), membreModel, privacy, cards);
        }
    }

    private void getTree(List<TreeModel> treeModels, MembreModel membreModel, List<CardModel> cards,
                         ResultCategoryData out) {
        ArrayList<TreeNode<ResultCategoryData.TreeData>> trees = new ArrayList<>();
        treeModels.stream()
                .filter(t -> t.getLevel() != 0)
                .sorted(Comparator.comparing(TreeModel::getLevel))
                .forEach(treeModel -> {
                    TreeNode<ResultCategoryData.TreeData> root = new TreeNode<>();
                    convertTree(treeModel, root, membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS, cards);
                    trees.add(root);
                });
        out.setTrees(trees);
    }

    public Uni<CombsArrayData> getAllCombArray(String uuid, SecurityCtx securityCtx) {
        return hasAccess(uuid, securityCtx)
                .chain(membreModel -> getAllCombArray_(uuid, membreModel));
    }

    public Uni<CombsArrayData> getAllCombArrayPublic(String uuid) {
        return getAllCombArray_(uuid, null);
    }

    private Uni<CombsArrayData> getAllCombArray_(String uuid, MembreModel membreModel) {
        return registerRepository.list("competition.uuid = ?1", uuid)
                .chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid)
                        .chain(matchModels -> cardRepository.list("competition.uuid = ?1", uuid)
                                .map(cards -> new Pair<>(registers,
                                        matchModels.stream().map(m -> new MatchModelExtend(m, cards)).toList()))))

                .map(pair -> {
                    List<RegisterModel> registers = pair.getKey();
                    List<MatchModelExtend> matchModels = pair.getValue();

                    CombsArrayData.CombsArrayDataBuilder builder = CombsArrayData.builder();

                    List<CombsArrayData.CombsData> combs = matchModels.stream()
                            .flatMap(m -> Stream.of(m.getC1_id(), m.getC2_id(), m.getC1_guest(), m.getC2_guest()))
                            .filter(Objects::nonNull)
                            .distinct()
                            .map(comb -> {
                                var builder2 = CombsArrayData.CombsData.builder();
                                CombStat stat = makeStat(matchModels, comb);

                                Categorie categorie = null;
                                String clubName = null;

                                Optional<RegisterModel> register = registers.stream()
                                        .filter(r -> Objects.equals(r.getMembre(), comb)).findFirst();
                                if (register.isPresent()) {
                                    categorie = register.get().getCategorie2();
                                    ClubModel club = register.get().getClub2();
                                    clubName = (club == null) ? trad.t("no.licence") : club.getName();
                                } else if (comb instanceof CompetitionGuestModel guestModel) {
                                    categorie = guestModel.getCategorie();
                                    clubName = guestModel.getClub();
                                } else if (comb instanceof MembreModel model) {
                                    categorie = model.getCategorie();
                                    clubName = (model.getClub() == null) ? trad.t("no.licence")
                                            : model.getClub().getName();
                                }

                                builder2.cat((categorie == null) ? "---" : categorie.getName(trad));
                                builder2.name(comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY));
                                builder2.w(stat.w);
                                builder2.l(stat.l);
                                builder2.ratioVictoire((stat.l == 0) ? stat.w : (float) stat.w / stat.l);
                                builder2.club(clubName);
                                builder2.pointMake(stat.pointMake);
                                builder2.pointTake(stat.pointTake);
                                builder2.ratioPoint(stat.getPointRate());

                                return builder2.build();
                            })
                            .sorted(Comparator.comparing(CombsArrayData.CombsData::name))
                            .toList();

                    builder.nb_insc(combs.size());
                    builder.tt_match((int) matchModels.stream().filter(MatchModelExtend::isEnd).count());
                    builder.point(matchModels.stream()
                            .filter(MatchModelExtend::isEnd)
                            .flatMap(m -> m.getScoresToCompute().stream())
                            .mapToInt(s -> s.getS1() + s.getS2()).sum());
                    builder.combs(combs);

                    return builder.build();
                });
    }

    public Uni<HashMap<String, String>> getCombList(String uuid, SecurityCtx securityCtx) {
        return hasAccess(uuid, securityCtx)
                .chain(membreModel -> getCombList(uuid, ResultPrivacy.REGISTERED_ONLY));
    }

    public Uni<HashMap<String, String>> getCombList(String uuid) {
        return getCombList(uuid, ResultPrivacy.PUBLIC);
    }

    private Uni<HashMap<String, String>> getCombList(String uuid, ResultPrivacy privacy) {
        return registerRepository.list("competition.uuid = ?1 AND membre.resultPrivacy <= ?2", uuid, privacy)
                .map(models -> {
                    HashMap<String, String> map = new HashMap<>();
                    models.forEach(
                            r -> map.put(r.getMembre().getName(), getCombTempId(r.getMembre().getId())));
                    return map;
                })
                .chain(map -> competitionGuestRepository.list("competition.uuid = ?1 AND lname != \"__team\"", uuid)
                        .map(models -> {
                            models.forEach(guestModel -> map.put(guestModel.getName(),
                                    getCombTempId(guestModel.getId() * -1)));
                            return map;
                        })
                );
    }

    public Uni<?> getCombArrayPublic(String uuid, String combTempId, SecurityCtx securityCtx) {
        return hasAccess(uuid, securityCtx)
                .chain(membreModel -> getCombArrayPublic(uuid, combTempId, ResultPrivacy.REGISTERED_ONLY));
    }

    public Uni<?> getCombArrayPublic(String uuid, String combTempId) {
        return getCombArrayPublic(uuid, combTempId, ResultPrivacy.PUBLIC);
    }

    private Uni<?> getCombArrayPublic(String uuid, String combTempId, ResultPrivacy privacy) {
        CombArrayData.CombArrayDataBuilder builder = CombArrayData.builder();

        Long id = getCombTempId(combTempId);
        if (id == null) {
            return Uni.createFrom().failure(new DForbiddenException(trad.t("comb.not.found")));
        }

        Uni<List<MatchModelExtend>> uni;
        if (id >= 0) {
            uni = registerRepository.find("membre.id = ?1 AND competition.uuid = ?2 AND membre.resultPrivacy <= ?3", id,
                            uuid, privacy).firstResult()
                    .chain(Unchecked.function(registerModel -> {
                        if (registerModel == null)
                            throw new DBadRequestException(trad.t("combattant.non.inscrit"));

                        builder.name(Utils.getFullName(registerModel.getMembre()));
                        builder.club((registerModel.getClub2() == null) ? trad.t("no.licence") :
                                registerModel.getClub2().getName());
                        builder.cat((registerModel.getCategorie2() == null) ? "---" :
                                registerModel.getCategorie2().getName(trad));

                        return matchRepository.list(
                                        "SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.comb c1g LEFT JOIN m.c2_guest.comb c2g " +
                                                "WHERE m.category.compet.uuid = ?1 AND (m.c1_id = ?2 OR m.c2_id = ?2 OR c1g = ?2 OR c2g = ?2)",
                                        uuid, registerModel.getMembre())
                                .chain(matchModels -> cardRepository.list("competition.uuid = ?1", uuid)
                                        .map(cards -> matchModels.stream().map(m -> new MatchModelExtend(m, cards))
                                                .toList()));
                    }));
        } else {
            uni = competitionGuestRepository.find("id = ?1 AND competition.uuid = ?2", -id, uuid).firstResult()
                    .chain(guestModel -> {
                        builder.name(Utils.getFullName(guestModel));
                        builder.club(guestModel.getClub());
                        builder.cat(
                                (guestModel.getCategorie() == null) ? "---" : guestModel.getCategorie().getName(trad));

                        return matchRepository.list(
                                        "SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.guest c1g LEFT JOIN m.c2_guest.guest c2g " +
                                                "WHERE m.category.compet.uuid = ?1 AND (m.c1_guest = ?2 OR m.c2_guest = ?2 OR c1g = ?2 OR c2g = ?2)",
                                        uuid, guestModel)
                                .chain(matchModels -> cardRepository.list("competition.uuid = ?1", uuid)
                                        .map(cards -> matchModels.stream().map(m -> new MatchModelExtend(m, cards))
                                                .toList()));
                    });
        }

        return uni.invoke(matchModels -> {
                    List<CategoryModel> pouleModels = matchModels.stream().map(MatchModelExtend::getCategory).distinct()
                            .toList();
                    List<CombArrayData.MatchsData> matchs = new ArrayList<>();

                    AtomicInteger sumW = new AtomicInteger();
                    AtomicInteger sumPointMake = new AtomicInteger(0);
                    AtomicInteger sumPointTake = new AtomicInteger(0);

                    for (MatchModelExtend matchModel : matchModels) {
                        if ((matchModel.getC1_id() == null && matchModel.getC1_guest() == null) ||
                                (matchModel.getC2_id() == null && matchModel.getC2_guest() == null))
                            continue;

                        var builder2 = CombArrayData.MatchsData.builder();
                        builder2.date(matchModel.getDate());
                        builder2.poule(pouleModels.stream().filter(p -> p.equals(matchModel.getCategory()))
                                .map(CategoryModel::getName).findFirst().orElse(""));
                        builder2.end(matchModel.isEnd());

                        AtomicInteger pointMake = new AtomicInteger();
                        AtomicInteger pointTake = new AtomicInteger();

                        if (matchModel.isC1(id)) {
                            builder2.adv(matchModel.getC2Name());
                            if (matchModel.isEnd()) {
                                matchModel.getScoresToCompute()
                                        .forEach(scoreEntity -> {
                                            pointMake.addAndGet(scoreEntity.getS1());
                                            pointTake.addAndGet(scoreEntity.getS2());
                                        });
                                builder2.score(matchModel.getScoresToPrint().stream()
                                        .map(s -> new Integer[]{s.getS1(), s.getS2()}).toList());
                            } else {
                                builder2.score(new ArrayList<>());
                            }
                            builder2.win(matchModel.isEnd() && matchModel.getWin() > 0);
                        } else {
                            builder2.adv(matchModel.getC1Name());
                            if (matchModel.isEnd()) {
                                matchModel.getScoresToCompute()
                                        .forEach(scoreEntity -> {
                                            pointMake.addAndGet(scoreEntity.getS2());
                                            pointTake.addAndGet(scoreEntity.getS1());
                                        });
                                builder2.score(matchModel.getScoresToPrint().stream()
                                        .map(s -> new Integer[]{s.getS2(), s.getS1()}).toList());
                            } else {
                                builder2.score(new ArrayList<>());
                            }
                            builder2.win(matchModel.isEnd() && matchModel.getWin() < 0);
                        }
                        builder2.eq(matchModel.isEnd() && matchModel.getWin() == 0);
                        builder2.ratio(
                                (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get());

                        sumPointMake.addAndGet(pointMake.get());
                        sumPointTake.addAndGet(pointTake.get());

                        matchs.add(builder2.build());
                        if (builder2.win)
                            sumW.getAndIncrement();
                    }

                    builder.totalWin(sumW.get());
                    builder.pointRatio(
                            (sumPointTake.get() == 0) ? sumPointMake.get() : (float) sumPointMake.get() / sumPointTake.get());
                    builder.pointMake(sumPointMake.get());
                    builder.pointTake(sumPointTake.get());

                    matchs.sort(Comparator.comparing(CombArrayData.MatchsData::poule)
                            .thenComparing(CombArrayData.MatchsData::adv));

                    builder.matchs(matchs);
                })
                .map(__ -> builder.build());
    }


    @Builder
    @RegisterForReflection
    public static record CombsArrayData(int nb_insc, int tt_match, long point, List<CombsData> combs) {
        @Builder
        @RegisterForReflection
        public static record CombsData(String cat, String club, String name, int w, int l, float ratioVictoire,
                                       float ratioPoint, int pointMake, int pointTake) {
        }
    }

    @Builder
    @RegisterForReflection
    public static record CombArrayData(String name, String club, String cat, int totalWin,
                                       float pointRatio, int pointMake, int pointTake, List<MatchsData> matchs) {
        @Builder
        @RegisterForReflection
        public static record MatchsData(Date date, String poule, String adv, List<Integer[]> score, float ratio,
                                        boolean win, boolean eq, boolean end) {
        }
    }

    public Uni<HashMap<String, Long>> getClubList(String uuid, SecurityCtx securityCtx) {
        return hasAccess(uuid, securityCtx).chain(__ -> getClubList(uuid));
    }

    public Uni<HashMap<String, Long>> getClubList(String uuid) {
        return registerRepository.list("competition.uuid = ?1", uuid)
                .map(registers -> {
                    HashMap<String, Long> registerMap = new HashMap<>();
                    registers.stream().map(RegisterModel::getClub2).distinct().filter(Objects::nonNull)
                            .forEach(registerClub -> registerMap.put(registerClub.getName(), registerClub.getId()));
                    return registerMap;
                })
                .chain(map -> competitionGuestRepository.list("competition.uuid = ?1", uuid)
                        .map(guests -> {
                            guests.stream().map(CompetitionGuestModel::getClub).distinct()
                                    .filter(Objects::nonNull)
                                    .forEach(guestClub -> map.putIfAbsent(guestClub,
                                            getClubTempId(guestClub)));
                            return map;
                        })
                );
    }

    public Uni<ClubArrayData> getClubArray(String uuid, Long id, SecurityCtx securityCtx) {
        return hasAccess(uuid, securityCtx).chain(membreModel -> getClubArray2(uuid, id, membreModel));
    }

    public Uni<ClubArrayData> getClubArray2(String uuid, Long id, MembreModel membreModel) {
        if (id < 0) {
            String clubName = getClubTempId(id);
            if (clubName == null) {
                return Uni.createFrom().failure(new DForbiddenException(trad.t("club.not.found")));
            }

            return competitionGuestRepository.list("competition.uuid = ?1 AND club = ?2", uuid, clubName)
                    .call(list -> {
                        if (list.isEmpty())
                            return Uni.createFrom().failure(new DBadRequestException(trad.t("club.not.found")));
                        return Uni.createFrom().voidItem();
                    })
                    .chain(guests -> matchRepository.list(
                                    "SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.guest c1g LEFT JOIN m.c2_guest.guest c2g " +
                                            "WHERE m.category.compet.uuid = ?1 AND (m.c1_guest IN ?2 OR m.c2_guest IN ?2 OR c1g IN ?2 OR c2g IN ?2)",
                                    uuid, guests)

                            .chain(mm -> cardRepository.list("competition.uuid = ?1", uuid)
                                    .map(cards ->
                                            getClubArray2(clubName, guests.stream().map(o -> (CombModel) o).toList(),
                                                    mm.stream().map(m -> new MatchModelExtend(m, cards)).toList(),
                                                    new ArrayList<>(), membreModel)
                                    )));
        } else {
            return cardRepository.list("competition.uuid = ?1", uuid)
                    .chain(cards -> clubRepository.findById(id).chain(clubModel ->
                            registerRepository.list("competition.uuid = ?1 AND membre.club = ?2", uuid, clubModel)
                                    .chain(registers -> matchRepository.list(
                                                    "SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.comb c1g LEFT JOIN m.c2_guest.comb c2g " +
                                                            "WHERE m.category.compet.uuid = ?1 AND (m.c1_id IN ?2 OR m.c2_id IN ?2 OR c1g IN ?2 OR c2g IN ?2)",
                                                    uuid, registers.stream().map(RegisterModel::getMembre).toList())
                                            .map(matchModels -> matchModels.stream()
                                                    .map(m -> new MatchModelExtend(m, cards)))
                                            .chain(s -> competitionGuestRepository.list(
                                                            "competition.uuid = ?1 AND club = ?2", uuid, clubModel.getName())
                                                    .chain(guests -> matchRepository.list(
                                                                    "SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.guest c1g LEFT JOIN m.c2_guest.guest c2g " +
                                                                            "WHERE m.category.compet.uuid = ?1 AND (m.c1_guest IN ?2 OR m.c2_guest IN ?2 OR c1g IN ?2 OR c2g IN ?2)",
                                                                    uuid, guests)
                                                            .map(mm -> new Pair<>(guests, Stream.concat(s, mm.stream()
                                                                            .map(m -> new MatchModelExtend(m, cards)))
                                                                    .distinct().toList()))
                                                    ))
                                            .map(p ->
                                                    getClubArray2(clubModel.getName(),
                                                            Stream.concat(
                                                                    registers.stream().map(RegisterModel::getMembre),
                                                                    p.getKey().stream()
                                                            ).toList(),
                                                            p.getValue(), registers, membreModel)
                                            ))));
        }
    }

    private ClubArrayData getClubArray2(String name, List<CombModel> combs, List<MatchModelExtend> matchModels,
                                        List<RegisterModel> registers, MembreModel membreModel) {
        ClubArrayData.ClubArrayDataBuilder builder = ClubArrayData.builder();
        builder.name(name);
        builder.nb_insc(combs.size());

        ArrayList<Long> win_ids = new ArrayList<>();
        ArrayList<Long> match_ids = new ArrayList<>();

        List<ClubArrayData.CombData> combData = combs.stream().map(comb -> {
                    var builder2 = ClubArrayData.CombData.builder();
                    CombStat stat = makeStat(matchModels, comb);

                    Categorie categorie = null;

                    Optional<RegisterModel> register = registers.stream()
                            .filter(r -> Objects.equals(r.getMembre(), comb)).findFirst();
                    if (register.isPresent()) {
                        categorie = register.get().getCategorie2();
                    } else if (comb instanceof CompetitionGuestModel guestModel) {
                        categorie = guestModel.getCategorie();
                    } else if (comb instanceof MembreModel model) {
                        categorie = model.getCategorie();
                    }

                    builder2.cat((categorie == null) ? "---" : categorie.getName(trad));
                    builder2.name(comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY));
                    builder2.w(stat.w);
                    builder2.l(stat.l);
                    builder2.ratioVictoire((stat.l == 0) ? stat.w : (float) stat.w / stat.l);
                    builder2.pointMake(stat.pointMake);
                    builder2.pointTake(stat.pointTake);
                    builder2.ratioPoint(stat.getPointRate());

                    win_ids.addAll(stat.win_ids);
                    match_ids.addAll(stat.match_ids);

                    return builder2.build();
                })
                .sorted(Comparator.comparing(ClubArrayData.CombData::name))
                .toList();

        builder.nb_match((int) match_ids.stream().distinct().count());
        builder.match_w((int) win_ids.stream().distinct().count());
        builder.ratioVictoire((float) combData.stream().filter(c -> c.l + c.w != 0)
                .mapToDouble(ClubArrayData.CombData::ratioVictoire).average().orElse(0L));
        builder.pointMake(combData.stream().mapToInt(ClubArrayData.CombData::pointMake).sum());
        builder.pointTake(combData.stream().mapToInt(ClubArrayData.CombData::pointTake).sum());
        builder.ratioPoint((float) combData.stream().filter(c -> c.l + c.w != 0)
                .mapToDouble(ClubArrayData.CombData::ratioPoint).average().orElse(0L));
        builder.combs(combData);

        return builder.build();
    }

    private static CombStat makeStat(List<MatchModelExtend> matchModels, CombModel comb) {
        CombStat stat = new CombStat();
        matchModels.stream()
                .filter(m -> m.isEnd() && (m.isC1(comb) || m.isC2(comb)))
                .forEach(matchModel -> {
                    stat.match_ids.add(matchModel.getId());

                    int win = matchModel.getWin();
                    if (win == 0) {
                        stat.score += 1;
                    } else if ((matchModel.isC1(comb) && win > 0) || matchModel.isC2(comb) && win < 0) {
                        stat.w++;
                        stat.win_ids.add(matchModel.getId());
                        stat.score += 2;
                    } else {
                        stat.l++;
                    }

                    matchModel.getScoresToCompute()
                            .forEach(score -> {
                                if (matchModel.isC1(comb)) {
                                    stat.pointMake += score.getS1();
                                    stat.pointTake += score.getS2();
                                } else {
                                    stat.pointMake += score.getS2();
                                    stat.pointTake += score.getS1();
                                }
                            });
                });
        return stat;
    }

    @Builder
    @RegisterForReflection
    public static record ClubArrayData(String name, int nb_insc, int nb_match, int match_w, float ratioVictoire,
                                       float ratioPoint, int pointMake, int pointTake, List<CombData> combs) {
        @Builder
        @RegisterForReflection
        public static record CombData(String cat, String name, int w, int l, float ratioVictoire,
                                      float ratioPoint, int pointMake, int pointTake) {
        }
    }

    public Uni<List<ClubClassement>> getAllClubArray(String uuid) {
        return getAllClubArray(uuid, true);
    }

    public Uni<List<ClubClassement>> getAllClubArray(String uuid, SecurityCtx securityCtx) {
        return hasAccess(uuid, securityCtx).chain(membreModel -> getAllClubArray(uuid, true));
    }

    public Uni<List<ClubClassement>> getAllClubArray(String uuid, boolean cache) {
        List<CardModel> cards = new java.util.ArrayList<>();

        //noinspection unchecked
        cacheClubClassement.invalidateIf(
                (p) -> ((Pair<Long, List<ClubClassement>>) p).getKey() > System.currentTimeMillis());

        if (!cache)
            cacheClubClassement.invalidate(uuid);

        return cacheClubClassement.getAsync(uuid, k -> cardRepository.list("competition.uuid = ?1", uuid)
                .invoke(__ -> System.out.println("Cache miss for club classement with uuid " + uuid))
                .invoke(cards::addAll)
                .chain(__ -> matchRepository.list("category.compet.uuid = ?1", uuid))
                .chain(matchs -> {
                    HashMap<CategoryModel, List<MatchModel>> map = new HashMap<>();
                    for (MatchModel match : matchs) {
                        if (!map.containsKey(match.getCategory()))
                            map.put(match.getCategory(), new java.util.ArrayList<>());
                        map.get(match.getCategory()).add(match);
                    }

                    return Multi.createFrom().iterable(map.entrySet())
                            .onItem().call(entry -> Mutiny.fetch(entry.getKey().getTree()))
                            .map(entry -> {
                                ResultCategoryData tmp = new ResultCategoryData();

                                getArray2(entry.getValue().stream().map(m -> new MatchModelExtend(m, cards)).toList(),
                                        null, tmp);
                                getClassementArray(entry.getKey(), null, cards, tmp);

                                return tmp;
                            })
                            .collect().asList();
                })
                .map(categoryData -> {
                    HashMap<String, ClubClassement> clubMap = new HashMap<>();

                    categoryData.forEach(c -> c.getClassement().stream().map(ResultCategoryData.ClassementData::comb)
                            .filter(Objects::nonNull)
                            .distinct()
                            .map(comb -> {
                                if (comb instanceof MembreModel membreModel2) {
                                    return (membreModel2.getClub() != null) ? membreModel2.getClub().getName() : "";
                                } else if (comb instanceof CompetitionGuestModel guestModel) {
                                    return guestModel.getClub();
                                }
                                return "";
                            })
                            .filter(s -> s != null && !s.isBlank() && !s.equals("Team"))
                            .distinct()
                            .forEach(clubName -> clubMap.putIfAbsent(clubName, new ClubClassement(clubName))));

                    System.out.println(clubMap.keySet());

                    categoryData.forEach(c -> c.getClassement().forEach(classementData -> {
                        System.out.println("Classement data for " + c.getName() + ": " + classementData.name() + " - " +
                                classementData.comb() + " - " + classementData.rank());
                        if (classementData.rank() > 3)
                            return;

                        System.out.println(
                                "Processing classement data for " + c.getName() + ": " + classementData.name());

                        if (classementData.comb() != null) {
                            String clubName = "";
                            if (classementData.comb() instanceof MembreModel membreModel2) {
                                clubName = (membreModel2.getClub() != null) ? membreModel2.getClub().getName() : "";
                            } else if (classementData.comb() instanceof CompetitionGuestModel guestModel) {
                                clubName = guestModel.getClub();
                            }
                            System.out.println("-> " + clubName + "- " + (clubName != null && !clubName.isBlank()
                                    && !clubName.equals("Team") && clubMap.containsKey(clubName)));

                            if (clubName != null && !clubName.isBlank()
                                    && !clubName.equals("Team") && clubMap.containsKey(clubName)) {
                                ClubClassement entity = clubMap.get(clubName);
                                entity.score[classementData.rank() - 1]++;
                                entity.tt_score += 4 - classementData.rank();
                            }
                        }
                    }));

                    return clubMap.values().stream()
                            .sorted(Comparator.comparingInt((ClubClassement c) -> c.tt_score)
                                    .thenComparingInt(c -> c.score[0])
                                    .thenComparingInt(c -> c.score[1])
                                    .thenComparingInt(c -> c.score[2]).reversed())
                            .toList();
                })
                .map(l -> new Pair<>(System.currentTimeMillis() + 60 * 1000L, l))
        ).map(Pair::getValue);
    }

    @Data
    @RegisterForReflection
    public static class ClubClassement {
        String name;
        Integer[] score = new Integer[]{0, 0, 0};
        int tt_score = 0;

        public ClubClassement(String name) {
            this.name = name;
        }
    }

    @RegisterForReflection
    public static class CombStat {
        public int w;
        public int l;
        public int score;
        public int pointMake;
        public int pointTake;
        public ArrayList<Long> win_ids = new ArrayList<>();
        public ArrayList<Long> match_ids = new ArrayList<>();

        public CombStat() {
            this.w = 0;
            this.l = 0;
            this.score = 0;
            this.pointMake = 0;
            this.pointTake = 0;
        }

        public float getPointRate() {
            return (pointTake == 0) ? pointMake : (float) pointMake / pointTake;
        }
    }

    private Uni<MembreModel> hasAccess(String uuid, SecurityCtx securityCtx) {
        return registerRepository.find("membre.userId = ?1 AND competition.uuid = ?2", securityCtx.getSubject(), uuid)
                .firstResult()
                .chain(Unchecked.function(o -> {
                    if (o != null)
                        return Uni.createFrom().item(o.getMembre());

                    return membreService.getByAccountId(securityCtx.getSubject()).chain(m -> {
                        if (securityCtx.isClubAdmin()) {
                            return registerRepository.count("membre.club = ?2 AND competition.uuid = ?1",
                                    uuid, m.getClub()).chain(c -> {
                                if (c > 0) return Uni.createFrom().item(m);

                                return compRepository.count("uuid = ?1 AND (owner = ?2 OR ?2 IN admin)",
                                                uuid, securityCtx.getSubject())
                                        .chain(c2 -> {
                                            if (c2 > 0) return Uni.createFrom().item(m);
                                            return Uni.createFrom().failure(new DForbiddenException(
                                                    trad.t("access.denied")));
                                        });
                            });
                        } else {
                            return compRepository.count("uuid = ?1 AND (owner = ?2 OR ?2 IN admin)", uuid,
                                            securityCtx.getSubject())
                                    .chain(c2 -> {
                                        if (c2 > 0) return Uni.createFrom().item(m);
                                        return Uni.createFrom()
                                                .failure(new DForbiddenException(trad.t("access.denied")));
                                    });
                        }
                    });
                }));
    }
}
