package fr.titionfire.ffsaf.ws.recv;

import fr.titionfire.ffsaf.data.model.*;
import fr.titionfire.ffsaf.data.repository.*;
import fr.titionfire.ffsaf.domain.entity.MatchEntity;
import fr.titionfire.ffsaf.domain.entity.MatchModelExtend;
import fr.titionfire.ffsaf.domain.entity.TreeEntity;
import fr.titionfire.ffsaf.domain.service.CardService;
import fr.titionfire.ffsaf.domain.service.ResultService;
import fr.titionfire.ffsaf.domain.service.TradService;
import fr.titionfire.ffsaf.rest.data.PresetData;
import fr.titionfire.ffsaf.rest.data.ResultCategoryData;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
import fr.titionfire.ffsaf.utils.TreeNode;
import fr.titionfire.ffsaf.ws.PermLevel;
import fr.titionfire.ffsaf.ws.send.SSCategorie;
import fr.titionfire.ffsaf.ws.send.SSMatch;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.runtime.annotations.RegisterForReflection;
import io.quarkus.websockets.next.UserData;
import io.quarkus.websockets.next.WebSocketConnection;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import lombok.Data;
import org.hibernate.reactive.mutiny.Mutiny;

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

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

    @Inject
    CategoryRepository categoryRepository;

    @Inject
    CompetitionRepository competitionRepository;

    @Inject
    MatchRepository matchRepository;

    @Inject
    TreeRepository treeRepository;

    @Inject
    CardService cardService;

    @Inject
    CardRepository cardRepository;

    @Inject
    CatPresetRepository catPresetRepository;

    @Inject
    ResultService resultService;

    @Inject
    TradService trad;

    private Uni<CategoryModel> getById(long id, WebSocketConnection connection) {
        return categoryRepository.findById(id)
                .invoke(Unchecked.consumer(o -> {
                    if (o == null)
                        throw new DNotFoundException(trad.t("categorie.non.trouver", connection));
                    if (!o.getCompet().getUuid().equals(connection.pathParam("uuid")))
                        throw new DForbiddenException(trad.t("permission.denied", connection));
                }));
    }

    @WSReceiver(code = "getAllCategory", permission = PermLevel.VIEW)
    public Uni<List<JustCategorie>> getAllCategory(WebSocketConnection connection, Object o) {
        return categoryRepository.list("compet.uuid", connection.pathParam("uuid"))
                .map(category -> category.stream().map(JustCategorie::from).toList());
    }

    @WSReceiver(code = "getFullCategory", permission = PermLevel.VIEW)
    public Uni<FullCategory> getFullCategory(WebSocketConnection connection, Long id) {
        FullCategory fullCategory = new FullCategory();

        return getById(id, connection)
                .invoke(cat -> {
                    fullCategory.setId(cat.getId());
                    fullCategory.setName(cat.getName());
                    fullCategory.setLiceName(cat.getLiceName());
                    fullCategory.setType(cat.getType());
                    fullCategory.setTreeAreClassement(cat.isTreeAreClassement());
                    fullCategory.setFullClassement(cat.isFullClassement());
                    fullCategory.setPreset(PresetData.fromModel(cat.getPreset()));
                })
                .call(cat -> Mutiny.fetch(cat.getMatchs())
                        .map(matchModels -> matchModels.stream().filter(o -> o.getCategory_ord() >= 0)
                                .map(MatchEntity::fromModel).toList())
                        .invoke(fullCategory::setMatches))
                .call(cat -> treeRepository.list("category = ?1 AND level != 0", cat.getId())
                        .map(treeModels -> treeModels.stream().map(TreeEntity::fromModel).toList())
                        .invoke(fullCategory::setTrees))
                .call(cat -> cardService.getAll(cat.getCompet())
                        .invoke(fullCategory::setCards))
                .map(__ -> fullCategory);
    }

    @WSReceiver(code = "createCategory", permission = PermLevel.ADMIN)
    public Uni<Long> createCategory(WebSocketConnection connection, JustCategorie categorie) {
        return competitionRepository.find("uuid", connection.pathParam("uuid")).firstResult()
                .chain(cm -> {
                    CategoryModel categoryModel = new CategoryModel();
                    categoryModel.setName(categorie.name);
                    categoryModel.setCompet(cm);
                    categoryModel.setMatchs(new ArrayList<>());
                    categoryModel.setTree(new ArrayList<>());
                    categoryModel.setType(categorie.type);
                    categoryModel.setLiceName(categorie.liceName);
                    categoryModel.setTreeAreClassement(categorie.treeAreClassement);
                    categoryModel.setFullClassement(categorie.fullClassement);

                    if (categorie.preset() != null)
                        return catPresetRepository.findById(categorie.preset().getId())
                                .invoke(categoryModel::setPreset)
                                .chain(__ -> categoryRepository.create(categoryModel));
                    return categoryRepository.create(categoryModel);
                })
                .invoke(cat -> SSCategorie.sendAddCategory(connection, cat))
                .map(CategoryModel::getId);
    }

    @WSReceiver(code = "createOrReplaceCategory", permission = PermLevel.ADMIN)
    public Uni<Long> createOrReplaceCategory(WebSocketConnection connection, JustCategorie categorie) {
        return matchRepository.list("category.compet.uuid = ?1 AND category.name = ?2", connection.pathParam("uuid"),
                        categorie.name)
                .chain(existing -> {
                    if (existing.isEmpty())
                        return createCategory(connection, categorie);

                    Map<Long, List<MatchModel>> matchesByCategory = existing.stream()
                            .filter(m -> m.getCategory() != null)
                            .collect(Collectors.groupingBy(m -> m.getCategory().getId()));

                    for (Map.Entry<Long, List<MatchModel>> entry : matchesByCategory.entrySet()) {
                        Long categoryId = entry.getKey();
                        List<MatchModel> matches = entry.getValue();

                        if (matches.stream().noneMatch(m -> !m.getScores().isEmpty() || m.isEnd()))
                            return Panache.withTransaction(() -> updateCategory(connection, categorie, categoryId)
                                            .call(__ -> treeRepository.delete("category = ?1", categoryId))
                                            .call(__ -> matchRepository.delete("category.id = ?1", categoryId)))
                                    .replaceWith(categoryId);
                    }
                    return createCategory(connection, categorie);
                });
    }

    @WSReceiver(code = "updateCategory", permission = PermLevel.ADMIN)
    public Uni<Void> updateCategory(WebSocketConnection connection, JustCategorie categorie) {
        return updateCategory(connection, categorie, categorie.id);
    }

    private Uni<Void> updateCategory(WebSocketConnection connection, JustCategorie categorie, Long id) {
        return getById(id, connection)
                .call(cat -> {
                    if (categorie.preset() == null) {
                        cat.setPreset(null);
                        return Uni.createFrom().item(cat);
                    } else {
                        return catPresetRepository.findById(categorie.preset().getId())
                                .invoke(cat::setPreset);
                    }
                })
                .chain(cat -> {
                    cat.setName(categorie.name);
                    cat.setLiceName(categorie.liceName);
                    cat.setType(categorie.type);
                    cat.setTreeAreClassement(categorie.treeAreClassement);
                    cat.setFullClassement(categorie.fullClassement);
                    return Panache.withTransaction(() -> categoryRepository.persist(cat));
                })
                .call(cat -> {
                    Uni<Long> uni = Uni.createFrom().nullItem();
                    if ((categorie.type() & 1) == 0)
                        uni = uni.chain(__ -> matchRepository.delete("category = ?1 AND category_ord >= 0", cat));
                    if ((categorie.type() & 2) == 0) {
                        uni = uni.chain(__ -> treeRepository.delete("category = ?1", cat.getId()))
                                .chain(__ -> matchRepository.delete("category = ?1 AND category_ord = -42", cat));
                    }
                    Uni<Long> finalUni = uni;
                    return Panache.withTransaction(() -> finalUni);
                })
                .invoke(cat -> SSCategorie.sendCategory(connection, cat))
                .replaceWithVoid();
    }

    private Uni<TreeModel> getOrCreateTreeNode(Long id, Long categorieId, int level) {
        if (id == null) {
            TreeModel treeModel = new TreeModel();
            treeModel.setCategory(categorieId);
            treeModel.setLevel(level);

            return Panache.withTransaction(() -> treeRepository.persistAndFlush(treeModel));
        } else {
            return Panache.withTransaction(
                    () -> treeRepository.find("match.id = ?1", id).firstResult()
                            .invoke(t -> t.setLevel(level))
                            .chain(t -> treeRepository.persist(t)));
        }
    }

    private Uni<Void> updateNode(TreeNode<Long> current, TreeModel currentTreeModel, CategoryModel category) {
        return Uni.createFrom().item(currentTreeModel)
                .chain(t -> {
                    if (current.getData() != null)
                        return Uni.createFrom().item(t);

                    MatchModel matchModel = new MatchModel();
                    matchModel.setCategory(category);
                    matchModel.setCategory_ord(-42);
                    matchModel.setEnd(false);

                    return matchRepository.create(matchModel).onItem()
                            .invoke(t::setMatch)
                            .chain(__ -> treeRepository.persistAndFlush(t));
                })
                .call(t -> {
                    if (current.getLeft() == null)
                        return Uni.createFrom().item(t);

                    return getOrCreateTreeNode(current.getLeft().getData(), category.getId(), 0)
                            .invoke(t::setLeft)
                            .call(treeModel -> updateNode(current.getLeft(), treeModel, category));
                })
                .call(t -> {
                    if (current.getRight() == null)
                        return Uni.createFrom().item(t);

                    return getOrCreateTreeNode(current.getRight().getData(), category.getId(), 0)
                            .invoke(t::setRight)
                            .call(treeModel -> updateNode(current.getRight(), treeModel, category));
                })
                .chain(t -> treeRepository.persist(t))
                .replaceWithVoid();
    }


    @WSReceiver(code = "updateTrees", permission = PermLevel.ADMIN)
    public Uni<Void> updateTrees(WebSocketConnection connection, TreeUpdate data) {
        return getById(data.categoryId, connection)
                .call(cat -> treeRepository.update("level = -1, left = NULL, right = NULL WHERE category = ?1",
                        cat.getId()))
                .call(cat -> {
                    Uni<?> uni = Uni.createFrom().voidItem();

                    for (int i = 0; i < data.trees().size(); i++) {
                        TreeNode<Long> current = data.trees().get(i);

                        int finalI = i;
                        uni = uni.chain(() -> getOrCreateTreeNode(current.getData(), cat.getId(), finalI + 1)
                                .call(treeModel -> updateNode(current, treeModel, cat)));

                    }
                    Uni<?> finalUni = uni;
                    return Panache.withTransaction(() -> finalUni);
                })
                .call(cat -> treeRepository.list("category = ?1 AND level = -1", cat.getId())
                        .map(l -> l.stream().map(o -> o.getMatch().getId()).toList())
                        .call(__ -> treeRepository.delete("category = ?1 AND level = -1", cat.getId()))
                        .call(ids -> matchRepository.delete("id IN ?1", ids)))
                .call(__ -> treeRepository.flush())
                .call(cat -> treeRepository.list("category = ?1 AND level != 0", cat.getId())
                        .map(treeModels -> treeModels.stream().map(TreeEntity::fromModel).toList())
                        .invoke(trees -> SSCategorie.sendTreeCategory(connection, trees)))
                .replaceWithVoid();
    }

    @WSReceiver(code = "deleteCategory", permission = PermLevel.ADMIN)
    public Uni<Void> deleteCategory(WebSocketConnection connection, Long id) {
        return getById(id, connection)
                .call(cat -> Panache.withTransaction(() -> treeRepository.delete("category = ?1", cat.getId())
                        .call(__ -> matchRepository.delete("category = ?1", cat))))
                .chain(cat -> Panache.withTransaction(() -> categoryRepository.delete(cat)))
                .invoke(__ -> SSCategorie.sendDelCategory(connection, id))
                .replaceWithVoid();
    }

    @WSReceiver(code = "listPreset", permission = PermLevel.VIEW)
    public Uni<List<PresetData>> listPreset(WebSocketConnection connection, Object o) {
        return catPresetRepository.list("competition.uuid", connection.pathParam("uuid"))
                .map(presets -> presets.stream().map(PresetData::fromModel).toList());
    }

    @WSReceiver(code = "createClassementMatchs", permission = PermLevel.TABLE)
    public Uni<Void> createClassementMatchs(WebSocketConnection connection, Long categoryId) {
        return getById(categoryId, connection)
                .call(cat -> {
                    PermLevel perm = PermLevel.valueOf(connection.userData().get(UserData.TypedKey.forString("prem")));
                    if (perm == PermLevel.TABLE) {
                        return matchRepository.list("category = ?1 AND category_ord = -42", cat.getId())
                                .chain(l -> l.stream().anyMatch(MatchModel::isEnd) ?
                                        Uni.createFrom().failure(
                                                new DForbiddenException(trad.t("err.match.termine", connection))) :
                                        Uni.createFrom().voidItem());
                    }
                    return Uni.createFrom().voidItem();
                })
                .call(cat -> treeRepository.list("category = ?1 AND level <= -10", cat.getId())
                        .map(l -> l.stream().map(o -> o.getMatch().getId()).toList())
                        .call(__ -> treeRepository.delete("category = ?1 AND level <= -10", cat.getId()))
                        .call(ids -> matchRepository.delete("id IN ?1", ids)))
                .call(cat -> Mutiny.fetch(cat.getTree()))
                .call(cat -> {
                    List<MatchModel> toSave = new ArrayList<>();
                    if (!cat.getTree().isEmpty()) {
                        for (TreeModel treeModel : cat.getTree())
                            cleanTree(treeModel, toSave);
                    }
                    return Panache.withTransaction(() -> matchRepository.persist(toSave))
                            .invoke(__ -> SSMatch.sendMatch(connection,
                                    toSave.stream().map(MatchEntity::fromModel).toList()));
                })
                .chain(cat -> Mutiny.fetch(cat.getMatchs())
                        .chain(list -> cardRepository.list("competition = ?1", cat.getCompet())
                                .map(c -> list.stream().map(m -> new MatchModelExtend(m, c)).toList()))
                        .map(matchModels -> {
                            ResultCategoryData out = new ResultCategoryData();
                            resultService.getArray2(matchModels, null, out);
                            out.getRankArray().remove('-');
                            return out;
                        })
                        .invoke(Unchecked.consumer(result -> {
                            if (result.getRankArray().size() != 2) {
                                throw new DForbiddenException(trad.t("configuration.non.supportee", connection));
                            }
                        }))
                        .chain(result -> {
                            List<MatchModel> toSave = new ArrayList<>();
                            List<MatchModel> toCreate = new ArrayList<>();
                            List<TreeModel> toSaveTree = new ArrayList<>();

                            int treeSize = 0;
                            List<TreeModel> lastNode = new ArrayList<>();
                            Optional<TreeModel> tree = cat.getTree().stream().filter(t -> t.getLevel() > 0)
                                    .min(Comparator.comparing(TreeModel::getLevel));
                            if (tree.isPresent()) {
                                tree.get().getChildrenAtDepth(tree.get().death() - 1, 0, lastNode);
                                treeSize = lastNode.size();
                            }

                            Iterator<List<ResultCategoryData.RankArray>> iterator = result.getRankArray().values()
                                    .iterator();
                            List<ResultCategoryData.RankArray> poule1 = iterator.next();
                            List<ResultCategoryData.RankArray> poule2 = iterator.next();

                            int maxToMixFill = Math.min(poule1.size(), poule2.size());
                            int maxToMixTreeFill = Math.min(treeSize, maxToMixFill);
                            for (int i = 0; i < maxToMixTreeFill; i++) {
                                CombModel comb1 = poule1.get(i).getComb();
                                CombModel comb2 = poule2.get(treeSize - i - 1).getComb();

                                fillMatchComb(lastNode.get(i).getMatch(), comb1, comb2);
                                toSave.add(lastNode.get(i).getMatch());
                            }

                            if (cat.isFullClassement()) {
                                for (int i = maxToMixTreeFill; i < maxToMixFill; i++) {
                                    MatchModel match = new MatchModel();
                                    match.setCategory(cat);
                                    match.setCategory_ord(-42);
                                    match.setEnd(false);

                                    CombModel comb1 = poule1.get(i).getComb();
                                    CombModel comb2 = poule2.get(i).getComb();

                                    fillMatchComb(match, comb1, comb2);
                                    toCreate.add(match);

                                    toSaveTree.add(new TreeModel(cat.getId(), -10 - i, match));
                                }
                            }

                            return Panache.withTransaction(() -> matchRepository.persist(toSave)
                                            .call(__ -> matchRepository.create(toCreate)))
                                    .call(__ -> toSaveTree.isEmpty() ? Uni.createFrom().voidItem()
                                            : Panache.withTransaction(() -> treeRepository.persist(toSaveTree)))
                                    .map(__ -> Stream.concat(toSave.stream(), toCreate.stream())
                                            .map(MatchEntity::fromModel).toList());
                        })
                )
                .onFailure().invoke(t -> System.out.println("error: " + t.getMessage()))
                .invoke(matchEntities -> SSMatch.sendMatch(connection, matchEntities))
                .call(__ -> treeRepository.list("category = ?1 AND level != 0", categoryId)
                        .map(treeModels -> treeModels.stream().map(TreeEntity::fromModel).toList())
                        .invoke(trees -> SSCategorie.sendTreeCategory(connection, trees)))
                .replaceWithVoid();
    }

    private void cleanTree(TreeModel treeModel, List<MatchModel> toSave) {
        MatchModel model = treeModel.getMatch();
        if (model != null) {
            model.setC1_id(null);
            model.setC1_guest(null);
            model.setC2_id(null);
            model.setC2_guest(null);
            model.setEnd(false);
            model.setDate(null);
            model.getScores().clear();

            toSave.add(model);
        }
        if (treeModel.getLeft() != null)
            cleanTree(treeModel.getLeft(), toSave);
        if (treeModel.getRight() != null)
            cleanTree(treeModel.getRight(), toSave);
    }

    private void fillMatchComb(MatchModel match, CombModel comb1, CombModel comb2) {
        if (comb1 instanceof MembreModel m)
            match.setC1_id(m);
        else if (comb1 instanceof CompetitionGuestModel g)
            match.setC1_guest(g);

        if (comb2 instanceof MembreModel m)
            match.setC2_id(m);
        else if (comb2 instanceof CompetitionGuestModel g)
            match.setC2_guest(g);
    }


    @RegisterForReflection
    public record JustCategorie(long id, String name, int type, String liceName, boolean treeAreClassement,
                                boolean fullClassement, PresetData preset) {
        public static JustCategorie from(CategoryModel m) {
            return new JustCategorie(m.getId(), m.getName(), m.getType(), m.getLiceName(), m.isTreeAreClassement(),
                    m.isFullClassement(), PresetData.fromModel(m.getPreset()));
        }
    }

    @RegisterForReflection
    public record TreeUpdate(long categoryId, List<TreeNode<Long>> trees) {
    }

    @Data
    @RegisterForReflection
    public static class FullCategory {
        long id;
        String name;
        int type;
        String liceName;
        boolean treeAreClassement = false;
        boolean fullClassement = false;
        PresetData preset;
        List<TreeEntity> trees = null;
        List<MatchEntity> matches;
        List<CardModel> cards;
    }
}
