package fr.titionfire.ffsaf.domain.service;

import fr.titionfire.ffsaf.data.model.*;
import fr.titionfire.ffsaf.data.repository.CategoryRepository;
import fr.titionfire.ffsaf.data.repository.CompetitionRepository;
import fr.titionfire.ffsaf.data.repository.MatchRepository;
import fr.titionfire.ffsaf.data.repository.RegisterRepository;
import fr.titionfire.ffsaf.rest.data.ResultCategoryData;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.utils.*;
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 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
    CategoryRepository categoryRepository;

    @Inject
    MatchRepository matchRepository;

    private static final ResourceBundle BUNDLE = ResourceBundle.getBundle("lang.String");

    public Uni<List<Object[]>> getList(SecurityCtx securityCtx) {
        return membreService.getByAccountId(securityCtx.getSubject())
                .chain(m -> registerRepository.list("membre = ?1", m))
                .onItem().transformToMulti(Multi.createFrom()::iterable)
                .onItem().call(r -> Mutiny.fetch(r.getCompetition()))
                .onItem().transform(r -> new Object[]{r.getCompetition().getUuid(), r.getCompetition().getName(),
                        r.getCompetition().getDate()})
                .collect().asList();
    }

    public Uni<List<ResultCategoryData>> getCategory(String uuid, SecurityCtx securityCtx) {
        return hasAccess(uuid, securityCtx)
                .chain(m -> categoryRepository.list("compet.uuid = ?1", uuid)
                        .chain(cats -> matchRepository.list("(c1_id = ?1 OR c2_id = ?1 OR True) AND category IN ?2", //TODO rm OR True
                                m.getMembre(), cats)))
                .map(matchModels -> {
                    HashMap<Long, List<MatchModel>> map = new HashMap<>();
                    for (MatchModel matchModel : matchModels) {
                        if (!map.containsKey(matchModel.getCategory().getId()))
                            map.put(matchModel.getCategory().getId(), new ArrayList<>());
                        map.get(matchModel.getCategory().getId()).add(matchModel);
                    }

                    return map.values();
                })
                .onItem()
                .transformToMulti(Multi.createFrom()::iterable)
                .onItem().call(list -> Mutiny.fetch(list.get(0).getCategory().getTree()))
                .onItem().transform(this::getData)
                .collect().asList();

    }

    private ResultCategoryData getData(List<MatchModel> matchModels) {
        ResultCategoryData out = new ResultCategoryData();

        CategoryModel categoryModel = matchModels.get(0).getCategory();
        out.setName(categoryModel.getName());
        out.setType(categoryModel.getType());

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

    private void getArray2(List<MatchModel> matchModels_, ResultCategoryData out) {
        List<MatchModel> matchModels = matchModels_.stream().filter(o -> o.getCategory_ord() >= 0).toList();

        HashMap<Character, List<MatchModel>> matchMap = new HashMap<>();
        for (MatchModel 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(MatchModel::getCategory_ord))
                    .map(ResultCategoryData.PouleArrayData::fromModel)
                    .toList();

            List<ResultCategoryData.RankArray> rankArray = matchEntities.stream()
                    .flatMap(m -> Stream.of(m.getC1Name(), m.getC2Name()))
                    .distinct()
                    .map(combName -> {
                        AtomicInteger w = new AtomicInteger(0);
                        AtomicInteger pointMake = new AtomicInteger(0);
                        AtomicInteger pointTake = new AtomicInteger(0);

                        matchEntities.stream()
                                .filter(m -> m.isEnd() && (m.getC1Name().equals(combName) || m.getC2Name()
                                        .equals(combName)))
                                .forEach(matchModel -> {
                                    int win = matchModel.win();
                                    if ((matchModel.getC1Name()
                                            .equals(combName) && win > 0) || matchModel.getC2Name()
                                            .equals(combName) && win < 0)
                                        w.getAndIncrement();

                                    for (ScoreEmbeddable score : matchModel.getScores()) {
                                        if (score.getS1() <= -900 || score.getS2() <= -900)
                                            continue;
                                        if (matchModel.getC1Name().equals(combName)) {
                                            pointMake.addAndGet(score.getS1());
                                            pointTake.addAndGet(score.getS2());
                                        } else {
                                            pointMake.addAndGet(score.getS2());
                                            pointTake.addAndGet(score.getS1());
                                        }
                                    }
                                });
                        float pointRate = (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get();

                        return new ResultCategoryData.RankArray(0, combName, w.get(),
                                pointMake.get(), pointTake.get(), pointRate);
                    })
                    .sorted(Comparator
                            .comparing(ResultCategoryData.RankArray::getWin)
                            .thenComparing(ResultCategoryData.RankArray::getPointRate).reversed())
                    .toList();
            out.getMatchs().put(c, matchs);

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

    private static void convertTree(TreeModel src, TreeNode<ResultCategoryData.TreeData> dst) {
        dst.setData(ResultCategoryData.TreeData.from(src.getMatch()));
        if (src.getLeft() != null) {
            dst.setLeft(new TreeNode<>());
            convertTree(src.getLeft(), dst.getLeft());
        }
        if (src.getRight() != null) {
            dst.setRight(new TreeNode<>());
            convertTree(src.getRight(), dst.getRight());
        }
    }

    private void getTree(List<TreeModel> treeModels, ResultCategoryData out) {
        ArrayList<TreeNode<ResultCategoryData.TreeData>> trees = new ArrayList<>();
        treeModels.stream().filter(t -> t.getLevel() != 0).forEach(treeModel -> {
            TreeNode<ResultCategoryData.TreeData> root = new TreeNode<>();
            convertTree(treeModel, root);
            trees.add(root);
        });
        out.setTrees(trees);
    }

    public Uni<CombsArrayData> getAllCombArray(String uuid, SecurityCtx securityCtx) {
        return hasAccess(uuid, securityCtx)
                .chain(cm_register -> registerRepository.list("competition.uuid = ?1", uuid)
                        .chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid)
                                .map(matchModels -> new Pair<>(registers, matchModels)))
                        .map(pair -> {
                            List<RegisterModel> registers = pair.getKey();
                            List<MatchModel> matchModels = pair.getValue();

                            CombsArrayData.CombsArrayDataBuilder builder = CombsArrayData.builder();

                            List<CombsArrayData.CombsData> combs = matchModels.stream()
                                    .flatMap(m -> Stream.of(m.getC1Name(), m.getC2Name()))
                                    .filter(Objects::nonNull)
                                    .distinct()
                                    .map(combName -> {
                                        var builder2 = CombsArrayData.CombsData.builder();
                                        AtomicInteger w = new AtomicInteger(0);
                                        AtomicInteger l = new AtomicInteger(0);
                                        AtomicInteger pointMake = new AtomicInteger();
                                        AtomicInteger pointTake = new AtomicInteger();

                                        matchModels.stream()
                                                .filter(m -> m.isEnd() && (m.getC1Name().equals(combName)
                                                        || m.getC2Name().equals(combName)))
                                                .forEach(matchModel -> {
                                                    int win = matchModel.win();
                                                    if ((combName.equals(matchModel.getC1Name()) && win > 0) ||
                                                            combName.equals(matchModel.getC2Name()) && win < 0) {
                                                        w.getAndIncrement();
                                                    } else {
                                                        l.getAndIncrement();
                                                    }

                                                    matchModel.getScores().stream()
                                                            .filter(s -> s.getS1() > -900 && s.getS2() > -900)
                                                            .forEach(score -> {
                                                                if (combName.equals(matchModel.getC1Name())) {
                                                                    pointMake.addAndGet(score.getS1());
                                                                    pointTake.addAndGet(score.getS2());
                                                                } else {
                                                                    pointMake.addAndGet(score.getS2());
                                                                    pointTake.addAndGet(score.getS1());
                                                                }
                                                            });
                                                });

                                        Categorie categorie = null;
                                        ClubModel club = null;

                                        Optional<RegisterModel> register = registers.stream()
                                                .filter(r -> r.getName().equals(combName)).findFirst();
                                        if (register.isPresent()) {
                                            categorie = register.get().getCategorie();
                                            club = register.get().getClub2();
                                        }

                                        builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE));
                                        builder2.name(combName);
                                        builder2.w(w.get());
                                        builder2.l(l.get());
                                        builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get());
                                        builder2.club((club == null) ? BUNDLE.getString("no.licence") : club.getName());
                                        builder2.pointMake(pointMake.get());
                                        builder2.pointTake(pointTake.get());
                                        builder2.ratioPoint(
                                                (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get());

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

                            builder.nb_insc(combs.size());
                            builder.tt_match((int) matchModels.stream().filter(MatchModel::isEnd).count());
                            builder.point(combs.stream().mapToInt(CombsArrayData.CombsData::pointMake).sum());
                            builder.combs(combs);

                            return 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) {
        }
    }

    public Uni<ClubArrayData> getClubArray(String uuid, SecurityCtx securityCtx) {
        ClubArrayData.ClubArrayDataBuilder builder = ClubArrayData.builder();

        return hasAccess(uuid, securityCtx)
                .invoke(cm_register -> builder.name(cm_register.getClub2().getName()))
                .chain(cm_register -> registerRepository.list("competition.uuid = ?1 AND membre.club = ?2", uuid,
                                cm_register.getClub2())
                        .chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid)
                                .map(matchModels -> new Pair<>(registers, matchModels)))
                        .map(pair -> {
                            List<RegisterModel> registers = pair.getKey();
                            List<MatchModel> matchModels = pair.getValue();

                            builder.nb_insc(registers.size());

                            AtomicInteger tt_win = new AtomicInteger(0);
                            AtomicInteger tt_match = new AtomicInteger(0);

                            List<ClubArrayData.CombData> combData = registers.stream()
                                    .map(register -> {

                                        var builder2 = ClubArrayData.CombData.builder();
                                        AtomicInteger w = new AtomicInteger(0);
                                        AtomicInteger l = new AtomicInteger(0);
                                        AtomicInteger pointMake = new AtomicInteger();
                                        AtomicInteger pointTake = new AtomicInteger();

                                        matchModels.stream()
                                                .filter(m -> m.isEnd() && (register.getMembre().equals(m.getC1_id())
                                                        || register.getMembre().equals(m.getC2_id())))
                                                .forEach(matchModel -> {
                                                    int win = matchModel.win();
                                                    if ((register.getMembre()
                                                            .equals(matchModel.getC1_id()) && win > 0) ||
                                                            register.getMembre()
                                                                    .equals(matchModel.getC2_id()) && win < 0) {
                                                        w.getAndIncrement();
                                                    } else {
                                                        l.getAndIncrement();
                                                    }

                                                    matchModel.getScores().stream()
                                                            .filter(s -> s.getS1() > -900 && s.getS2() > -900)
                                                            .forEach(score -> {
                                                                if (register.getMembre()
                                                                        .equals(matchModel.getC1_id())) {
                                                                    pointMake.addAndGet(score.getS1());
                                                                    pointTake.addAndGet(score.getS2());
                                                                } else {
                                                                    pointMake.addAndGet(score.getS2());
                                                                    pointTake.addAndGet(score.getS1());
                                                                }
                                                            });
                                                });

                                        Categorie categorie = register.getCategorie();
                                        if (categorie == null)
                                            categorie = register.getMembre().getCategorie();

                                        builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE));
                                        builder2.name(register.getName());
                                        builder2.w(w.get());
                                        builder2.l(l.get());
                                        builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get());
                                        builder2.pointMake(pointMake.get());
                                        builder2.pointTake(pointTake.get());
                                        builder2.ratioPoint(
                                                (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get());

                                        tt_win.addAndGet(w.get());
                                        tt_match.addAndGet(w.get() + l.get());

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

                            builder.nb_match(tt_match.get());
                            builder.match_w(tt_win.get());
                            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();
                        })
                );
    }

    @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) {
        }
    }

    private Uni<RegisterModel> hasAccess(String uuid, SecurityCtx securityCtx) {
        return registerRepository.find("membre.userId = ?1 AND competition.uuid = ?2", securityCtx.getSubject(), uuid)
                .firstResult()
                .invoke(Unchecked.consumer(o -> {
                    if (o == null)
                        throw new DForbiddenException("Access denied");
                }));
    }

    private Uni<RegisterModel> hasAccess(Long compId, SecurityCtx securityCtx) {
        return registerRepository.find("membre.userId = ?1 AND competition.id = ?2", securityCtx.getSubject(), compId)
                .firstResult()
                .invoke(Unchecked.consumer(o -> {
                    if (o == null)
                        throw new DForbiddenException("Access denied");
                }));
    }
}
