package fr.titionfire.ffsaf.domain.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import fr.titionfire.ffsaf.data.model.AffiliationModel;
import fr.titionfire.ffsaf.data.model.ClubModel;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.data.repository.AffiliationRepository;
import fr.titionfire.ffsaf.data.repository.ClubRepository;
import fr.titionfire.ffsaf.data.repository.CombRepository;
import fr.titionfire.ffsaf.net2.ServerCustom;
import fr.titionfire.ffsaf.net2.data.SimpleClubModel;
import fr.titionfire.ffsaf.net2.request.SReqClub;
import fr.titionfire.ffsaf.rest.data.*;
import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
import fr.titionfire.ffsaf.rest.from.FullClubForm;
import fr.titionfire.ffsaf.rest.from.PartClubForm;
import fr.titionfire.ffsaf.utils.*;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.PanacheQuery;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import io.quarkus.vertx.VertxContextSupport;
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 org.eclipse.microprofile.config.inject.ConfigProperty;
import org.hibernate.reactive.mutiny.Mutiny;

import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;

import static fr.titionfire.ffsaf.net2.Client_Thread.MAPPER;

@WithSession
@ApplicationScoped
public class ClubService {

    @Inject
    ClubRepository repository;

    @Inject
    AffiliationRepository affiliationRepository;

    @Inject
    ServerCustom serverCustom;

    @Inject
    CombRepository combRepository;

    @Inject
    KeycloakService keycloakService;

    @ConfigProperty(name = "upload_dir")
    String media;

    @Inject
    LoggerService ls;

    @Inject
    TradService trad;

    public SimpleClubModel findByIdOptionalClub(long id) throws Throwable {
        return VertxContextSupport.subscribeAndAwait(
                () -> Panache.withTransaction(() -> repository.findById(id).map(SimpleClubModel::fromModel)));
    }

    public Collection<SimpleClubModel> findAllClub() throws Throwable {
        return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(
                () -> repository.findAll().list()
                        .map(clubModels -> clubModels.stream().map(SimpleClubModel::fromModel).toList())));
    }

    public Uni<List<ClubModel>> getAll() {
        return repository.listAll();
    }

    public Uni<?> setClubId(Long id, String id1) {
        return repository.findById(id).chain(clubModel -> {
            ls.logChange("KC UUID", clubModel.getClubId(), id1, clubModel);
            clubModel.setClubId(id1);
            return Panache.withTransaction(() -> repository.persist(clubModel))
                    .call(() -> ls.append());
        });
    }

    public Uni<PageResult<SimpleClubList>> search(Integer limit, int page, String search, String country) {
        if (search == null)
            search = "";
        search = search + "%";

        PanacheQuery<ClubModel> query;

        if (country == null || country.isBlank())
            query = repository.find("LOWER(name) LIKE LOWER(?1)",
                    Sort.ascending("name"), search).page(Page.ofSize(limit));
        else
            query = repository.find("LOWER(name) LIKE LOWER(?1) AND country LIKE ?2",
                    Sort.ascending("name"), search, country + "%").page(Page.ofSize(limit));
        return getPageResult(query, limit, page);
    }

    private Uni<PageResult<SimpleClubList>> getPageResult(PanacheQuery<ClubModel> query, int limit, int page) {
        return Uni.createFrom().item(new PageResult<SimpleClubList>())
                .invoke(result -> result.setPage(page))
                .invoke(result -> result.setPage_size(limit))
                .call(result -> query.count().invoke(result::setResult_count))
                .call(result -> query.pageCount()
                        .invoke(Unchecked.consumer(pages -> {
                            if (page > pages) throw new DBadRequestException("Page out of range");
                        }))
                        .invoke(result::setPage_count))
                .call(result -> query.page(Page.of(page, limit)).list()
                        .map(membreModels -> membreModels.stream().map(SimpleClubList::fromModel).toList())
                        .invoke(result::setResult));
    }

    public Uni<ClubModel> getById(long id) {
        return repository.findById(id).call(m -> Mutiny.fetch(m.getContact()));
    }

    public Uni<ClubModel> getByClubId(String clubId) {
        return repository.find("clubId", clubId).firstResult();
    }

    public Uni<ClubModel> getOfUser(SecurityCtx securityCtx) {
        return combRepository.find("userId = ?1", securityCtx.getSubject()).firstResult()
                .invoke(Unchecked.consumer(m -> {
                    if (m == null || m.getClub() == null)
                        throw new DNotFoundException(trad.t("club.non.trouve"));
                }))
                .map(MembreModel::getClub)
                .call(club -> Mutiny.fetch(club.getContact()));
    }

    public Uni<List<DeskMember>> getClubDesk(Consumer<ClubModel> consumer, long id) {
        return repository.findById(id).invoke(consumer)
                .chain(club -> combRepository.list("club = ?1", club))
                .map(combs -> combs.stream()
                        .filter(o -> o.getRole() != null && o.getRole().level >= RoleAsso.MEMBREBUREAU.level)
                        .sorted((o1, o2) -> o2.getRole().level - o1.getRole().level)
                        .map(DeskMember::fromModel)
                        .toList());
    }

    public Uni<List<VerySimpleMembre>> getMembers(SecurityCtx securityCtx) {
        return combRepository.find("userId = ?1", securityCtx.getSubject()).firstResult()
                .invoke(Unchecked.consumer(m -> {
                    if (m == null || m.getClub() == null)
                        throw new DNotFoundException(trad.t("club.non.trouve"));
                    if (!securityCtx.isInClubGroup(m.getClub().getId()))
                        throw new DForbiddenException();
                }))
                .chain(m -> combRepository.list("club = ?1", m.getClub()))
                .map(membreModels -> membreModels.stream()
                        .map(m -> new VerySimpleMembre(m.getLname(), m.getFname(), m.getLicence())).toList());
    }

    public Uni<String> updateOfUser(SecurityCtx securityCtx, PartClubForm form) {
        TypeReference<HashMap<Contact, String>> typeRef = new TypeReference<>() {
        };

        return combRepository.find("userId = ?1", securityCtx.getSubject()).firstResult()
                .invoke(Unchecked.consumer(m -> {
                    if (m == null || m.getClub() == null)
                        throw new DNotFoundException(trad.t("club.non.trouve"));
                    if (!securityCtx.isInClubGroup(m.getClub().getId()))
                        throw new DForbiddenException();
                }))
                .map(MembreModel::getClub)
                .call(club -> Mutiny.fetch(club.getContact()))
                .chain(Unchecked.function(club -> {
                    ls.logChange("Contact interne", club.getContact_intern(), form.getContact_intern(), club);
                    club.setContact_intern(form.getContact_intern());
                    ls.logChange("Adresse administrative", club.getAddress(), form.getAddress(), club);
                    club.setAddress(form.getAddress());

                    try {
                        if (!Objects.equals(club.getContact(), MAPPER.readValue(form.getContact(), typeRef)))
                            ls.logUpdate("Contact(s)...", club);
                        club.setContact(MAPPER.readValue(form.getContact(), typeRef));
                    } catch (JsonProcessingException e) {
                        throw new DBadRequestException(trad.t("erreur.de.format.des.contacts"));
                    }

                    ls.logChange("Lieux d'entrainements", club.getTraining_location(), form.getTraining_location(),
                            club);
                    club.setTraining_location(form.getTraining_location());
                    ls.logChange("Horaires d'entrainements", club.getTraining_day_time(), form.getTraining_day_time(),
                            club);
                    club.setTraining_day_time(form.getTraining_day_time());
                    return Panache.withTransaction(() -> repository.persist(club)).call(() -> ls.append());
                }))
                .map(__ -> "OK");
    }

    public Uni<String> update(long id, FullClubForm input) {
        AtomicBoolean nameChange = new AtomicBoolean(false);

        return repository.findById(id).call(m -> Mutiny.fetch(m.getContact()))
                .onItem().transformToUni(Unchecked.function(m -> {
                    TypeReference<HashMap<Contact, String>> typeRef = new TypeReference<>() {
                    };

                    if (!input.getName().equals(m.getName())) {
                        m.setName(input.getName());
                        nameChange.set(true);
                    }
                    m.setCountry(input.getCountry());
                    m.setInternational(input.isInternational());

                    if (!input.isInternational()) {
                        ls.logChange("Lieux d'entrainements", m.getTraining_location(), input.getTraining_location(),
                                m);
                        m.setTraining_location(input.getTraining_location());
                        ls.logChange("Horaires d'entrainements", m.getTraining_day_time(), input.getTraining_day_time(),
                                m);
                        m.setTraining_day_time(input.getTraining_day_time());
                        ls.logChange("Contact interne", m.getContact_intern(), input.getContact_intern(), m);
                        m.setContact_intern(input.getContact_intern());
                        if (input.getState_id() != null && !input.getState_id().isBlank()) {
                            ls.logChange("N° SIRET", m.getClubId(), input.getState_id(), m);
                            m.setStateId(input.getState_id());
                        }
                        ls.logChange("Adresse administrative", m.getAddress(), input.getAddress(), m);
                        m.setAddress(input.getAddress());

                        try {
                            if (!Objects.equals(m.getContact(), MAPPER.readValue(input.getContact(), typeRef)))
                                ls.logUpdate("Contact(s)...", m);
                            m.setContact(MAPPER.readValue(input.getContact(), typeRef));
                        } catch (JsonProcessingException e) {
                            throw new DBadRequestException(trad.t("erreur.de.format.des.contacts"));
                        }
                    }
                    return Panache.withTransaction(() -> repository.persist(m)).call(() -> ls.append());
                }))
                .call(clubModel -> nameChange.get() ? keycloakService.updateGroupFromClub(
                        clubModel) // update group in keycloak
                        : Uni.createFrom().nullItem())
                .invoke(membreModel -> SReqClub.sendIfNeed(serverCustom.clients,
                        SimpleClubModel.fromModel(membreModel)))
                .map(__ -> "OK");
    }

    public Uni<Long> add(FullClubForm input) {
        TypeReference<HashMap<Contact, String>> typeRef = new TypeReference<>() {
        };

        return Uni.createFrom().nullItem()
                .chain(() -> {
                    ClubModel clubModel = new ClubModel();

                    clubModel.setName(input.getName());
                    clubModel.setCountry(input.getCountry());
                    clubModel.setInternational(input.isInternational());
                    clubModel.setNo_affiliation(null);
                    if (!input.isInternational()) {
                        clubModel.setTraining_location(input.getTraining_location());
                        clubModel.setTraining_day_time(input.getTraining_day_time());
                        clubModel.setContact_intern(input.getContact_intern());
                        if (input.getState_id() != null && !input.getState_id().isBlank())
                            clubModel.setStateId(input.getState_id());
                        clubModel.setAddress(input.getAddress());

                        try {
                            clubModel.setContact(MAPPER.readValue(input.getContact(), typeRef));
                        } catch (JsonProcessingException ignored) {
                        }
                    }

                    return Panache.withTransaction(() -> repository.persist(clubModel));
                })
                .call(clubModel -> ls.logAAdd(clubModel))
                .call(clubModel -> keycloakService.getGroupFromClub(clubModel)) // create group in keycloak
                .invoke(clubModel -> SReqClub.sendAddIfNeed(serverCustom.clients, SimpleClubModel.fromModel(clubModel)))
                .map(ClubModel::getId);
    }

    public Uni<?> delete(long id) {
        return repository.findById(id)
                .chain(club -> combRepository.list("club = ?1", club)
                        .map(combModels -> combModels.stream().peek(combModel -> {
                            combModel.setClub(null);
                            combModel.setRole(RoleAsso.MEMBRE);
                        }).toList())
                        .call(list -> list.stream().noneMatch(m -> m.getUserId() != null) ?
                                Uni.createFrom().voidItem() :
                                Uni.join().all(list.stream().filter(m -> m.getUserId() != null)
                                                .map(m -> keycloakService.clearUser(m.getUserId())).toList())
                                        .andCollectFailures())
                        .chain(list -> list.isEmpty() ? Uni.createFrom().voidItem() :
                                Panache.withTransaction(() -> combRepository.persist(list)))
                        .map(o -> club)
                )
                .call(clubModel -> (clubModel.getClubId() == null) ? Uni.createFrom()
                        .voidItem() : keycloakService.removeClubGroup(clubModel.getClubId()))
                .invoke(membreModel -> SReqClub.sendRmIfNeed(serverCustom.clients, id))
                .call(clubModel -> ls.logADelete(clubModel))
                .chain(clubModel -> Panache.withTransaction(() -> repository.delete(clubModel)))
                .call(__ -> Utils.deleteMedia(id, media, "ppClub"))
                .call(__ -> Utils.deleteMedia(id, media, "clubStatus"));
    }

    public Uni<RenewAffData> getRenewData(long id, List<Long> mIds) {
        RenewAffData data = new RenewAffData();

        return repository.findById(id)
                .call(clubModel -> Mutiny.fetch(clubModel.getAffiliations()))
                .invoke(clubModel -> {
                    data.setName(clubModel.getName());
                    data.setState_id(clubModel.getStateId());
                    data.setAddress(clubModel.getAddress());
                    data.setContact(clubModel.getContact_intern());
                    data.setSaison(
                            clubModel.getAffiliations().stream().max(Comparator.comparing(AffiliationModel::getSaison))
                                    .map(AffiliationModel::getSaison).map(i -> Math.min(i + 1, Utils.getSaison() + 1))
                                    .orElse(Utils.getSaison()));
                })
                .chain(club -> combRepository.list("id IN ?1", mIds))
                .invoke(combs -> data.setMembers(combs.stream()
                        .filter(o -> o.getRole() != null && o.getRole().level >= RoleAsso.MEMBREBUREAU.level)
                        .sorted((o1, o2) -> o2.getRole().level - o1.getRole().level)
                        .map(RenewAffData.RenewMember::new)
                        .toList()))
                .map(o -> data);
    }

    public Uni<List<ClubMapData>> getMapData() {
        return affiliationRepository.list("saison >= ?1 AND club.international = ?2", Utils.getSaison() - 1, false)
                .toMulti().flatMap(list -> Multi.createFrom().iterable(list))
                .call(affiliationModel -> Mutiny.fetch(affiliationModel.getClub().getContact()))
                .map(affiliationModel -> {
                    ClubModel clubModel = affiliationModel.getClub();
                    ClubMapData data = new ClubMapData();

                    data.setName(clubModel.getName());
                    data.setUuid(clubModel.getClubId());
                    if (clubModel.getTraining_location() != null) {
                        try {
                            MAPPER.readTree(clubModel.getTraining_location()).forEach(l -> {
                                ClubMapData.Location loc = new ClubMapData.Location();
                                loc.setLat(l.get("lat").asDouble());
                                loc.setLng(l.get("lng").asDouble());
                                loc.setAddr(l.get("text").asText());
                                data.training_location.add(loc);
                            });
                        } catch (JsonProcessingException ignored) {
                        }
                    }
                    data.setTraining_day_time(clubModel.getTraining_day_time());
                    data.setContact(clubModel.getContact());

                    return data;
                }).collect().asList();
    }
}
