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.TreeEntity;
import fr.titionfire.ffsaf.domain.service.TradService;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
import fr.titionfire.ffsaf.utils.ScoreEmbeddable;
import fr.titionfire.ffsaf.ws.PermLevel;
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.panache.common.Sort;
import io.quarkus.runtime.annotations.RegisterForReflection;
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 org.jboss.logging.Logger;

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

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

    @Inject
    MatchRepository matchRepository;

    @Inject
    CombRepository combRepository;

    @Inject
    CategoryRepository categoryRepository;

    @Inject
    TreeRepository treeRepository;

    @Inject
    CompetitionGuestRepository competitionGuestRepository;

    @Inject
    TradService trad;

    @Inject
    RState rState;

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

    private Uni<MatchModel> creatMatch(CategoryModel categoryModel, AddMatch m) {
        return Uni.createFrom().item(() -> {
                    MatchModel matchModel = new MatchModel();

                    matchModel.setCategory(categoryModel);
                    matchModel.setCategory_ord(m.categorie_ord);
                    matchModel.setEnd(false);
                    matchModel.setPoule(m.poule);

                    return matchModel;
                })
                .call(mm -> m.c1() >= 0 ?
                        combRepository.findById(m.c1()).invoke(mm::setC1_id) :
                        competitionGuestRepository.findById(m.c1() * -1).invoke(mm::setC1_guest))
                .call(mm -> m.c2() >= 0 ?
                        combRepository.findById(m.c2()).invoke(mm::setC2_id) :
                        competitionGuestRepository.findById(m.c2() * -1).invoke(mm::setC2_guest));
    }

    @WSReceiver(code = "addMatch", permission = PermLevel.ADMIN)
    public Uni<Void> addMatch(WebSocketConnection connection, AddMatch m) {
        return categoryRepository.findById(m.categorie)
                .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));
                }))
                .chain(categoryModel -> creatMatch(categoryModel, m))
                .chain(mm -> Panache.withTransaction(() -> matchRepository.create(mm)))
                .invoke(mm -> SSMatch.sendMatch(connection, MatchEntity.fromModel(mm)))
                .replaceWithVoid();
    }

    @WSReceiver(code = "updateMatchComb", permission = PermLevel.ADMIN)
    public Uni<Void> updateMatchComb(WebSocketConnection connection, MatchComb match) {
        return getById(match.id(), connection)
                .call(mm -> match.c1() != null ?
                        match.c1() >= 0 ?
                                combRepository.findById(match.c1()).invoke(model -> {
                                    mm.setC1_id(model);
                                    mm.setC1_guest(null);
                                }) :
                                competitionGuestRepository.findById(match.c1() * -1).invoke(model -> {
                                    mm.setC1_id(null);
                                    mm.setC1_guest(model);

                                }) :
                        Uni.createFrom().nullItem().invoke(__ -> {
                            mm.setC1_id(null);
                            mm.setC1_guest(null);
                        }))
                .call(mm -> match.c2() != null ?
                        match.c2() >= 0 ?
                                combRepository.findById(match.c2()).invoke(model -> {
                                    mm.setC2_id(model);
                                    mm.setC2_guest(null);
                                }) :
                                competitionGuestRepository.findById(match.c2() * -1).invoke(model -> {
                                    mm.setC2_id(null);
                                    mm.setC2_guest(model);
                                }) :
                        Uni.createFrom().nullItem().invoke(__ -> {
                            mm.setC2_id(null);
                            mm.setC2_guest(null);
                        }))
                .chain(mm -> Panache.withTransaction(() -> matchRepository.persist(mm)))
                .invoke(mm -> SSMatch.sendMatch(connection, MatchEntity.fromModel(mm)))
                .replaceWithVoid();
    }

    @WSReceiver(code = "updateMatchOrder", permission = PermLevel.ADMIN)
    public Uni<Void> updateMatchComb(WebSocketConnection connection, MatchOrder order) {
        return getById(order.id(), connection)
                .call(m -> matchRepository.update(
                        "category_ord = category_ord + 1 WHERE category_ord >= ?1 AND category_ord < ?2", order.pos,
                        m.getCategory_ord()))
                .call(m -> matchRepository.update(
                        "category_ord = category_ord - 1 WHERE category_ord <= ?1 AND category_ord > ?2", order.pos,
                        m.getCategory_ord()))
                .invoke(m -> m.setCategory_ord(order.pos))
                .call(m -> Panache.withTransaction(() -> matchRepository.persist(m)))
                .invoke(mm -> SSMatch.sendMatchOrder(connection, order))
                .replaceWithVoid();
    }

    @WSReceiver(code = "updateMatchScore", permission = PermLevel.TABLE)
    public Uni<Void> updateMatchScore(WebSocketConnection connection, MatchScore score) {
        return getById(score.matchId(), connection)
                .chain(matchModel -> {
                    int old_win = matchModel.win();
                    Optional<ScoreEmbeddable> optional = matchModel.getScores().stream()
                            .filter(s -> s.getN_round() == score.n_round()).findAny();
                    boolean b = score.s1() != -1000 || score.s2() != -1000;
                    if (optional.isPresent()) {
                        if (b) {
                            optional.get().setS1(score.s1());
                            optional.get().setS2(score.s2());
                        } else {
                            matchModel.getScores().remove(optional.get());
                        }
                    } else if (b) {
                        matchModel.getScores().add(new ScoreEmbeddable(score.n_round(), score.s1(), score.s2()));
                    }

                    return Panache.withTransaction(() -> matchRepository.persist(matchModel))
                            .call(mm -> {
                                if (mm.isEnd() && mm.win() != old_win && mm.getCategory_ord() == -42) {
                                    return updateEndAndTree(mm, new ArrayList<>())
                                            .invoke(l -> SSMatch.sendMatch(connection, l));
                                }
                                return Uni.createFrom().nullItem();
                            });
                })
                .invoke(mm -> SSMatch.sendMatch(connection, MatchEntity.fromModel(mm)))
                .replaceWithVoid();
    }

    @WSReceiver(code = "updateMatchEnd", permission = PermLevel.TABLE)
    public Uni<Void> updateMatchEnd(WebSocketConnection connection, MatchEnd matchEnd) {
        List<MatchEntity> toSend = new ArrayList<>();

        return getById(matchEnd.matchId(), connection)
                .chain(mm -> {
                    if (mm.getCategory_ord() == -42 && mm.win() == 0) { // Tournois
                        mm.setDate(null);
                        mm.setEnd(false);
                    } else {
                        mm.setDate((matchEnd.end) ? new Date() : null);
                        mm.setEnd(matchEnd.end);
                    }
                    return Panache.withTransaction(() -> matchRepository.persist(mm));
                })
                .invoke(mm -> toSend.add(MatchEntity.fromModel(mm)))
                .invoke(mm -> rState.setMatchEnd(connection, matchEnd))
                .chain(mm -> updateEndAndTree(mm, toSend))
                .invoke(__ -> SSMatch.sendMatch(connection, toSend))
                .replaceWithVoid();
    }

    private Uni<List<MatchEntity>> updateEndAndTree(MatchModel mm, List<MatchEntity> toSend) {
        return (mm.getCategory_ord() != -42) ?
                Uni.createFrom().item(toSend) :
                treeRepository.list("category = ?1 AND level != 0", Sort.ascending("level"), mm.getCategory().getId())
                        .chain(treeModels -> {
                            List<TreeModel> node = treeModels.stream().flatMap(t -> t.flat().stream()).toList();
                            List<TreeEntity> trees = treeModels.stream().map(TreeEntity::fromModel).toList();
                            for (int i = 0; i < trees.size() - 1; i++) {
                                TreeEntity.setAssociated(trees.get(i), trees.get(i + 1));
                            }

                            TreeEntity root = trees.stream()
                                    .filter(t -> t.getMatchNode(mm.getId()) != null)
                                    .findFirst()
                                    .orElseThrow();

                            TreeEntity currentNode = root.getMatchNode(mm.getId());
                            if (currentNode == null) {
                                LOGGER.error(
                                        "currentNode is empty for " + mm.getId() + " in " + mm.getCategory().getId());
                                return Uni.createFrom().voidItem();
                            }

                            TreeEntity parent = TreeEntity.getParent(root, currentNode);
                            if (parent == null) {
                                LOGGER.error("parent is empty for " + mm.getId() + " in " + mm.getCategory().getId());
                                return Uni.createFrom().voidItem();
                            }

                            int w = mm.win();
                            MembreModel toSetWin = null;
                            MembreModel toSetLose = null;
                            CompetitionGuestModel toSetWinGuest = null;
                            CompetitionGuestModel toSetLoseGuest = null;

                            if (mm.isEnd() && w != 0) {
                                toSetWin = (w > 0) ? mm.getC1_id() : mm.getC2_id();
                                toSetLose = (w > 0) ? mm.getC2_id() : mm.getC1_id();
                                toSetWinGuest = (w > 0) ? mm.getC1_guest() : mm.getC2_guest();
                                toSetLoseGuest = (w > 0) ? mm.getC2_guest() : mm.getC1_guest();
                            }

                            MatchModel modelWin = node.stream()
                                    .filter(n -> Objects.equals(n.getId(), parent.getId()))
                                    .findAny()
                                    .map(TreeModel::getMatch)
                                    .orElseThrow();

                            MatchModel modelLose = (parent.getAssociatedNode() == null) ? null : node.stream()
                                    .filter(n -> Objects.equals(n.getId(), parent.getAssociatedNode().getId()))
                                    .findAny()
                                    .map(TreeModel::getMatch)
                                    .orElseThrow();

                            if (currentNode.equals(parent.getLeft())) {
                                modelWin.setC1_id(toSetWin);
                                modelWin.setC1_guest(toSetWinGuest);
                                if (modelLose != null) {
                                    modelLose.setC1_id(toSetLose);
                                    modelLose.setC1_guest(toSetLoseGuest);
                                }
                            } else if (currentNode.equals(parent.getRight())) {
                                modelWin.setC2_id(toSetWin);
                                modelWin.setC2_guest(toSetWinGuest);
                                if (modelLose != null) {
                                    modelLose.setC2_id(toSetLose);
                                    modelLose.setC2_guest(toSetLoseGuest);
                                }
                            }

                            return Panache.withTransaction(() -> matchRepository.persist(modelWin)
                                    .invoke(mm2 -> toSend.add(MatchEntity.fromModel(mm2)))
                                    .call(__ -> modelLose == null ? Uni.createFrom().nullItem() :
                                            matchRepository.persist(modelLose)
                                                    .invoke(mm2 -> toSend.add(MatchEntity.fromModel(mm2)))
                                    )
                                    .replaceWithVoid());
                        })
                        .map(__ -> toSend);
    }

    @WSReceiver(code = "deleteMatch", permission = PermLevel.ADMIN)
    public Uni<Void> deleteMatch(WebSocketConnection connection, Long idMatch) {
        return getById(idMatch, connection)
                .map(__ -> idMatch)
                .chain(l -> Panache.withTransaction(() -> matchRepository.delete("id = ?1", l)))
                .invoke(__ -> SSMatch.sendDeleteMatch(connection, idMatch))
                .replaceWithVoid();
    }

    @WSReceiver(code = "recalculateMatch", permission = PermLevel.ADMIN)
    public Uni<Void> recalculateMatch(WebSocketConnection connection, RecalculateMatch data) {
        ArrayList<MatchEntity> matches = new ArrayList<>();
        return categoryRepository.findById(data.categorie)
                .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));
                }))
                .call(cm -> data.matchesToRemove.isEmpty() ? Uni.createFrom().voidItem() :
                        (Panache.withTransaction(
                                        () -> matchRepository.delete("id IN ?1 AND category = ?2", data.matchesToRemove, cm))
                                .invoke(__ -> SSMatch.sendDeleteMatch(connection, data.matchesToRemove))))
                .call(cm -> Panache.withSession(() -> matchRepository.list("id IN ?1 AND category = ?2",
                                Stream.concat(data.matchOrderToUpdate.keySet().stream(),
                                                data.matchPouleToUpdate.keySet().stream())
                                        .distinct().toList(), cm)
                        .invoke(matchModels -> matchModels.forEach(model -> {
                            if (data.matchPouleToUpdate.containsKey(model.getId()))
                                model.setPoule(data.matchPouleToUpdate.get(model.getId()));
                            if (data.matchOrderToUpdate.containsKey(model.getId()))
                                model.setCategory_ord(data.matchOrderToUpdate.get(model.getId()));
                        }))
                        .call(mm -> mm.isEmpty() ? Uni.createFrom().voidItem() :
                                Panache.withTransaction(() -> matchRepository.persist(mm)))
                        .invoke(mm -> matches.addAll(mm.stream().map(MatchEntity::fromModel).toList())))
                )
                .chain(categoryModel -> {
                            Uni<List<MatchModel>> uni = Uni.createFrom().item(new ArrayList<>());
                            for (AddMatch match : data.newMatch)
                                uni = uni.call(l -> creatMatch(categoryModel, match).invoke(l::add));
                            Uni<List<MatchModel>> finalUni = uni;
                            return Panache.withSession(() -> finalUni);
                        }
                )
                .chain(mm -> mm.isEmpty() ? Uni.createFrom().voidItem() :
                        Panache.withTransaction(() -> matchRepository.create(mm))
                                .invoke(__ -> matches.addAll(mm.stream().map(MatchEntity::fromModel).toList())))
                .invoke(__ -> SSMatch.sendMatch(connection, matches))
                .replaceWithVoid();
    }

    @RegisterForReflection
    public record MatchComb(long id, Long c1, Long c2) {
    }

    @RegisterForReflection
    public record MatchScore(long matchId, int n_round, int s1, int s2) {
    }

    @RegisterForReflection
    public record MatchEnd(long matchId, boolean end) {
    }

    @RegisterForReflection
    public record MatchOrder(long id, long pos) {
    }

    @RegisterForReflection
    public record AddMatch(long categorie, long categorie_ord, char poule, long c1, long c2) {
    }

    public record RecalculateMatch(long categorie, List<AddMatch> newMatch, HashMap<Long, Integer> matchOrderToUpdate,
                                   HashMap<Long, Character> matchPouleToUpdate, List<Long> matchesToRemove) {
    }
}
