package fr.titionfire.ffsaf.domain.service;

import fr.titionfire.ffsaf.data.model.*;
import fr.titionfire.ffsaf.data.repository.*;
import fr.titionfire.ffsaf.rest.client.SirenService;
import fr.titionfire.ffsaf.rest.client.StateIdService;
import fr.titionfire.ffsaf.rest.data.SimpleAffiliation;
import fr.titionfire.ffsaf.rest.data.SimpleReqAffiliation;
import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
import fr.titionfire.ffsaf.rest.from.AffiliationRequestForm;
import fr.titionfire.ffsaf.rest.from.AffiliationRequestSaveForm;
import fr.titionfire.ffsaf.utils.Genre;
import fr.titionfire.ffsaf.utils.GradeArbitrage;
import fr.titionfire.ffsaf.utils.SequenceType;
import fr.titionfire.ffsaf.utils.Utils;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.mailer.Mail;
import io.quarkus.mailer.reactive.ReactiveMailer;
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.eclipse.microprofile.rest.client.inject.RestClient;
import org.hibernate.reactive.mutiny.Mutiny;
import org.jboss.logging.Logger;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;

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

    @Inject
    CombRepository combRepository;

    @Inject
    ClubRepository clubRepository;

    @Inject
    AffiliationRequestRepository repositoryRequest;

    @Inject
    AffiliationRepository repository;

    @Inject
    KeycloakService keycloakService;

    @Inject
    SequenceRepository sequenceRepository;

    @Inject
    LicenceRepository licenceRepository;

    @Inject
    ReactiveMailer reactiveMailer;

    @Inject
    LoggerService ls;

    @Inject
    VirusScannerService scanner;

    @RestClient
    StateIdService stateIdService;

    @RestClient
    SirenService sirenService;

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

    @ConfigProperty(name = "notif.affRequest.mail")
    List<String> mails;

    @Inject
    TradService trad;

    public Uni<List<AffiliationRequestModel>> getAllReq() {
        return repositoryRequest.listAll();
    }

    public Uni<AffiliationRequestModel> pre_save(AffiliationRequestForm form, boolean unique) {
        AffiliationRequestModel affModel = form.toModel();
        int currentSaison = Utils.getSaison();
        List<String> out = new ArrayList<>();
        out.add(affModel.getState_id());

        return Uni.createFrom().item(affModel)
                .invoke(Unchecked.consumer(model -> {
                    if (model.getSaison() != currentSaison && model.getSaison() != currentSaison + 1) {
                        throw new DBadRequestException(trad.t("saison.non.valid"));
                    }
                }))
                .chain(() -> ((affModel.getState_id().charAt(0) == 'W') ? stateIdService.get_rna(
                                affModel.getState_id()) : sirenService.get_unite(affModel.getState_id())
                                .chain(stateIdService::getAssoDataFromUnit)).onItem().transform(o -> {
                                    if (o.getRna() != null && !o.getRna().isBlank())
                                        out.add(o.getRna());
                                    if (o.getSiren() != null && !o.getSiren().isBlank())
                                        out.add(o.getSiren());
                                    if (o.getIdentite().getSiret_siege() != null && !o.getIdentite().getSiret_siege().isBlank())
                                        out.add(o.getIdentite().getSiret_siege());
                                    return out;
                                }).onFailure().recoverWithItem(out)
                                .chain(a -> repositoryRequest.count("state_id IN ?1 and saison = ?2",
                                        out, affModel.getSaison()))
                                .onItem().invoke(Unchecked.consumer(count -> {
                                    if (count != 0 && unique) {
                                        throw new DBadRequestException(trad.t("demande.d.affiliation.deja.existante"));
                                    }
                                }))
                )
                .chain(() -> clubRepository.find("StateId IN ?1", out).firstResult().chain(club ->
                        repository.count("club = ?1 and saison = ?2", club, affModel.getSaison())))
                .onItem().invoke(Unchecked.consumer(count -> {
                    if (count != 0) {
                        throw new DBadRequestException(trad.t("affiliation.deja.existante"));
                    }
                }))
                .map(o -> affModel)
                .call(model -> ((model.getM1_lincence() != -1) ? combRepository.find("licence",
                                model.getM1_lincence()).count().invoke(Unchecked.consumer(count -> {
                            if (count == 0) {
                                throw new DBadRequestException(trad.t("licence.membre.n.1.inconnue"));
                            }
                        })) : Uni.createFrom().nullItem())
                )
                .call(model -> ((model.getM2_lincence() != -1) ? combRepository.find("licence",
                                model.getM2_lincence()).count().invoke(Unchecked.consumer(count -> {
                            if (count == 0) {
                                throw new DBadRequestException(trad.t("licence.membre.n.2.inconnue"));
                            }
                        })) : Uni.createFrom().nullItem())
                )
                .call(model -> ((model.getM3_lincence() != -1) ? combRepository.find("licence",
                                model.getM3_lincence()).count().invoke(Unchecked.consumer(count -> {
                            if (count == 0) {
                                throw new DBadRequestException(trad.t("licence.membre.n.3.inconnue"));
                            }
                        })) : Uni.createFrom().nullItem())
                );
    }

    public Uni<?> saveEdit(AffiliationRequestForm form) {
        return pre_save(form, false)
                .chain(model -> repositoryRequest.findById(form.getId())
                        .onItem().ifNull().failWith(new DNotFoundException(trad.t("demande.d.affiliation.non.trouve")))
                        .chain(origine -> {
                            origine.setName(model.getName());
                            origine.setAddress(model.getAddress());
                            origine.setContact(model.getContact());
                            origine.setM1_lname(model.getM1_lname());
                            origine.setM1_fname(model.getM1_fname());
                            origine.setM1_lincence(model.getM1_lincence());
                            origine.setM1_role(model.getM1_role());
                            origine.setM1_email(model.getM1_email());
                            origine.setM2_lname(model.getM2_lname());
                            origine.setM2_fname(model.getM2_fname());
                            origine.setM2_lincence(model.getM2_lincence());
                            origine.setM2_role(model.getM2_role());
                            origine.setM2_email(model.getM2_email());
                            origine.setM3_lname(model.getM3_lname());
                            origine.setM3_fname(model.getM3_fname());
                            origine.setM3_lincence(model.getM3_lincence());
                            origine.setM3_role(model.getM3_role());
                            origine.setM3_email(model.getM3_email());

                            return Panache.withTransaction(() -> repositoryRequest.persist(origine));
                        }));
    }

    public Uni<String> save(AffiliationRequestForm form) {
        LOGGER.debug("Affiliation Request Created");
        LOGGER.debug(form.toString());

        // noinspection ReactiveStreamsUnusedPublisher
        return pre_save(form, true)
                .chain(model -> Panache.withTransaction(() -> repositoryRequest.persist(model)))
                .onItem()
                .invoke(m -> Utils.uploadFile(scanner, form.getLogo(), m.getId(), media, "aff_request/logo"))
                .invoke(m -> Utils.uploadFile(scanner, form.getStatus(), m.getId(), media, "aff_request/status"))
                .call(model -> reactiveMailer.send(
                        Mail.withText("no-reply@ffsaf.fr",
                                        "[NOTIF] FFSAF - Nouvelle demande d'affiliation",
                                        String.format(
                                                """
                                                        Une nouvelle demande d'affiliation a été déposée sur l'intranet pour le club: %s.
                                                        """, model.getName())
                                ).setFrom("FFSAF <no-reply@ffsaf.fr>")
                                .addBcc(mails.toArray(String[]::new))
                ))
                .map(__ -> "Ok");
    }

    public Uni<?> saveAdmin(AffiliationRequestSaveForm form) {
        LOGGER.debug("Affiliation Request Saved");
        LOGGER.debug(form.toString());

        return repositoryRequest.findById(form.getId())
                .onItem().ifNull().failWith(new DNotFoundException(trad.t("demande.d.affiliation.non.trouve")))
                .map(model -> {
                    model.setName(form.getName());
                    model.setState_id(form.getState_id());
                    model.setAddress(form.getAddress());
                    model.setContact(form.getContact());

                    if (form.getM1_mode() == 2) {
                        model.setM1_lname(form.getM1_lname());
                        model.setM1_fname(form.getM1_fname());
                    } else {
                        model.setM1_lincence(
                                form.getM1_lincence() == null ? 0 : Integer.parseInt(form.getM1_lincence()));
                    }
                    model.setM1_role(form.getM1_role());
                    if (form.getM1_email_mode() == 0)
                        model.setM1_email(form.getM1_email());

                    if (form.getM2_mode() == 2) {
                        model.setM2_lname(form.getM2_lname());
                        model.setM2_fname(form.getM2_fname());
                    } else {
                        model.setM2_lincence(
                                form.getM2_lincence() == null ? 0 : Integer.parseInt(form.getM2_lincence()));
                    }
                    model.setM2_role(form.getM2_role());
                    if (form.getM2_email_mode() == 0)
                        model.setM2_email(form.getM2_email());

                    if (form.getM3_mode() == 2) {
                        model.setM3_lname(form.getM3_lname());
                        model.setM3_fname(form.getM3_fname());
                    } else {
                        model.setM3_lincence(
                                form.getM3_lincence() == null ? 0 : Integer.parseInt(form.getM3_lincence()));
                    }
                    model.setM3_role(form.getM3_role());
                    if (form.getM3_email_mode() == 0)
                        model.setM3_email(form.getM3_email());

                    return model;
                })
                .chain(model -> Panache.withTransaction(() -> repositoryRequest.persist(model)))
                .onItem()
                .invoke(m -> Utils.uploadFile(scanner, form.getLogo(), m.getId(), media, "aff_request/logo"))
                .invoke(m -> Utils.uploadFile(scanner, form.getStatus(), m.getId(), media, "aff_request/status"))
                .map(__ -> "Ok");
    }

    private Uni<?> setMembre(AffiliationRequestSaveForm.Member member, ClubModel club, int saison) {
        return Uni.createFrom().nullItem().chain(__ -> {
                    if (member.getMode() == 2) {
                        MembreModel membreModel = new MembreModel();
                        membreModel.setFname(member.getFname());
                        membreModel.setLname(member.getLname().toUpperCase());
                        membreModel.setClub(club);
                        membreModel.setRole(member.getRole());
                        membreModel.setEmail(member.getEmail());
                        membreModel.setGrade_arbitrage(GradeArbitrage.NA);
                        membreModel.setGenre(Genre.NA);
                        membreModel.setCountry("FR");
                        return Panache.withTransaction(() ->
                                combRepository.persist(membreModel)
                                        .chain(m -> sequenceRepository.getNextValueInTransaction(SequenceType.Licence)
                                                .invoke(l -> m.setLicence(Math.toIntExact(l)))
                                                .chain(() -> combRepository.persist(m))));
                    } else {
                        return combRepository.find("licence", Integer.parseInt(member.getLicence())).firstResult()
                                .onItem().ifNull().switchTo(() -> {
                                    MembreModel membreModel = new MembreModel();
                                    membreModel.setFname(member.getFname());
                                    membreModel.setLname(member.getLname().toUpperCase());
                                    return Panache.withTransaction(
                                            () -> sequenceRepository.getNextValueInTransaction(SequenceType.Licence)
                                                    .invoke(l -> membreModel.setLicence(Math.toIntExact(l)))
                                                    .chain(() -> combRepository.persist(membreModel)));
                                })
                                .map(m -> {
                                    m.setClub(club);
                                    m.setRole(member.getRole());
                                    m.setEmail(member.getEmail());
                                    return m;
                                }).call(m -> Panache.withTransaction(() -> combRepository.persist(m)));
                    }
                })
                .call(m -> ((m.getUserId() == null) ? keycloakService.initCompte(m.getId())
                        .onFailure().invoke(t -> LOGGER.warnf("Failed to init account: %s", t.getMessage())).onFailure()
                        .recoverWithNull() :
                        keycloakService.setClubGroupMembre(m, club).map(__ -> m.getUserId()))
                        .call(userId -> keycloakService.setAutoRoleMembre(userId, m.getRole(), m.getGrade_arbitrage()))
                        .call(userId -> keycloakService.setEmail(userId, m.getEmail())))
                .call(m -> Mutiny.fetch(m.getLicences())
                        .call(l1 -> l1 != null && l1.stream().anyMatch(l -> l.getSaison() == saison) ?
                                Uni.createFrom().nullItem() :
                                Panache.withTransaction(() -> licenceRepository.persist(
                                                new LicenceModel(null, m, club.getId(), saison, null, true, false)))
                                        .call(licenceModel -> ls.logA(LogModel.ActionType.ADD, m.getObjectName(),
                                                licenceModel))));
    }

    public Uni<?> accept(AffiliationRequestSaveForm form) {
        LOGGER.debug("Affiliation Request Accepted");
        LOGGER.debug(form.toString());

        return repositoryRequest.findById(form.getId())
                .onItem().ifNull().failWith(new DNotFoundException(trad.t("demande.d.affiliation.non.trouve")))
                .chain(req ->
                        clubRepository.find("StateId = ?1", form.getState_id()).firstResult()
                                .chain(model -> (model == null) ? acceptNew(form, req) : acceptOld(form, req, model))
                                .call(club -> setMembre(form.new Member(1), club, req.getSaison()).onFailure()
                                        .recoverWithNull()
                                        .call(__ -> setMembre(form.new Member(2), club, req.getSaison()).onFailure()
                                                .recoverWithNull()
                                                .call(___ -> setMembre(form.new Member(3), club, req.getSaison()))))
                                .onItem()
                                .invoke(model -> Utils.uploadFile(scanner, form.getLogo(), form.getId(), media,
                                        "aff_request/logo"))
                                .invoke(model -> Utils.uploadFile(scanner, form.getStatus(), form.getId(), media,
                                        "aff_request/status"))
                                .onItem()
                                .call(model -> Utils.moveMedia(form.getId(), model.getId(), media, "aff_request/logo",
                                        "ppClub"))
                                .call(model -> Utils.moveMedia(form.getId(), model.getId(), media, "aff_request/status",
                                        "clubStatus"))
                )
                .map(__ -> "Ok");
    }

    private Uni<ClubModel> acceptNew(AffiliationRequestSaveForm form, AffiliationRequestModel model) {
        LOGGER.debug("New Club Accepted");
        return Uni.createFrom().nullItem()
                .chain(() -> {
                    ClubModel club = new ClubModel();
                    club.setName(form.getName());
                    club.setCountry("FR");
                    club.setStateId(form.getState_id());
                    club.setAddress(form.getAddress());
                    club.setContact_intern(form.getContact());
                    club.setAffiliations(new ArrayList<>());
                    return Panache.withTransaction(() -> clubRepository.persist(club)
                            .chain(c -> sequenceRepository.getNextValueInTransaction(SequenceType.Affiliation)
                                    .invoke(c::setNo_affiliation)
                                    .chain(() -> clubRepository.persist(c))
                                    .chain(() -> repositoryRequest.delete(model))
                            )
                            .chain(() -> repository.persist(new AffiliationModel(null, club, model.getSaison())))
                            .map(c -> club));
                })
                .call(club -> reactiveMailer.send(
                        Mail.withHtml(form.getM1_email(), "FFSAF - Creation de votre compte sur l'intranet",
                                        String.format(Utils.HTML_HEADER, "FFSAF - Creation de votre compte sur l'intranet") +
                                                String.format("""
                                                                <p>Votre demande d'affiliation pour le club <span class="highlight">%s</span> a été acceptée.</p>
                                                                <p>LLe numéro d'affiliation de votre club est le <span class="highlight">%d</span>.</p>
                                                                """,
                                                        club.getName(), club.getNo_affiliation()) +
                                                Utils.HTML_FOOTER
                                )
                                .setText(String.format("""
                                        Bonjour,
                                        
                                        Votre demande d'affiliation pour le club %s a été acceptée.
                                        Le numéro d'affiliation de votre club est le %d.
                                        
                                        Cordialement,
                                        L'équipe de la FFSAF
                                        """, club.getName(), club.getNo_affiliation()))
                                .setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("contact@ffsaf.fr")
                                .addTo(form.getM2_email(), form.getM3_email())
                ));
    }

    private Uni<ClubModel> acceptOld(AffiliationRequestSaveForm form, AffiliationRequestModel model, ClubModel club) {
        AtomicBoolean nameChange = new AtomicBoolean(false);
        LOGGER.debug("Old Club Accepted");
        return Uni.createFrom().nullItem()
                .chain(() -> {
                    if (!form.getName().equals(club.getName())) {
                        club.setName(form.getName());
                        nameChange.set(true);
                    }
                    club.setCountry("FR");
                    club.setStateId(form.getState_id());
                    club.setAddress(form.getAddress());
                    club.setContact_intern(form.getContact());
                    return Panache.withTransaction(() -> clubRepository.persist(club)
                                    .chain(() -> repository.persist(new AffiliationModel(null, club, model.getSaison())))
                                    .chain(() -> repositoryRequest.delete(model)))
                            .call(() -> nameChange.get() ? keycloakService.updateGroupFromClub(
                                    club) // update group in keycloak
                                    : Uni.createFrom().nullItem());
                })
                .map(__ -> club);
    }

    public Uni<SimpleReqAffiliation> getRequest(long id) {
        return repositoryRequest.findById(id).map(SimpleReqAffiliation::fromModel)
                .onItem().ifNull().failWith(new DNotFoundException(trad.t("demande.d.affiliation.non.trouve")))
                .call(out -> clubRepository.find("StateId = ?1", out.getStateId()).firstResult().invoke(c -> {
                            if (c != null) {
                                out.setClub(c.getId());
                                out.setClub_name(c.getName());
                                out.setClub_no_aff(c.getNo_affiliation());
                            }
                        })
                );
    }

    public Uni<List<SimpleAffiliation>> getCurrentSaisonAffiliation() {
        return repositoryRequest.list("saison = ?1 or saison = ?1 + 1", Utils.getSaison())
                .map(models -> models.stream()
                        .map(model -> new SimpleAffiliation(model.getId() * -1, model.getState_id(), model.getSaison(),
                                false)).toList())
                .chain(aff -> repository.list("saison = ?1", Utils.getSaison())
                        .map(models -> models.stream().map(SimpleAffiliation::fromModel).toList())
                        .map(aff2 -> Stream.concat(aff2.stream(), aff.stream()).toList())
                );
    }

    public Uni<List<SimpleAffiliation>> getAffiliation(long id) {
        return clubRepository.findById(id)
                .onItem().ifNull().failWith(new DNotFoundException(trad.t("club.non.trouve")))
                .call(model -> Mutiny.fetch(model.getAffiliations()))
                .chain(model -> repositoryRequest.list("state_id = ?1", model.getStateId())
                        .map(reqs -> reqs.stream().map(req ->
                                new SimpleAffiliation(req.getId() * -1, model.getStateId(), req.getSaison(), false)))
                        .map(aff2 -> Stream.concat(aff2,
                                model.getAffiliations().stream().map(SimpleAffiliation::fromModel)).toList())
                );
    }

    public Uni<SimpleAffiliation> setAffiliation(long id, int saison) {
        return clubRepository.findById(id)
                .onItem().ifNull().failWith(new DNotFoundException(trad.t("club.non.trouve")))
                .call(model -> Mutiny.fetch(model.getAffiliations()))
                .invoke(Unchecked.consumer(club -> {
                    if (club.getAffiliations().stream().anyMatch(affiliation -> affiliation.getSaison() == saison)) {
                        throw new DBadRequestException(trad.t("affiliation.deja.existante"));
                    }
                }))
                .chain(club ->
                        Panache.withTransaction(() -> repository.persist(new AffiliationModel(null, club, saison))
                                .chain(c -> (club.getNo_affiliation() != null) ? Uni.createFrom().item(c) :
                                        sequenceRepository.getNextValueInTransaction(SequenceType.Affiliation)
                                                .invoke(club::setNo_affiliation)
                                                .chain(() -> clubRepository.persist(club))
                                                .map(o -> c)
                                )))
                .map(SimpleAffiliation::fromModel);
    }

    public Uni<?> deleteAffiliation(long id) {
        return Panache.withTransaction(() -> repository.deleteById(id));
    }

    public Uni<?> deleteReqAffiliation(long id, String reason, boolean federationAdmin) {
        return repositoryRequest.findById(id)
                .call(aff -> federationAdmin ? reactiveMailer.send(
                        Mail.withHtml(aff.getM1_email(), "FFSAF - Votre demande d'affiliation a été rejetée.",
                                        String.format(Utils.HTML_HEADER, "FFSAF - Votre demande d'affiliation a été rejetée.") +
                                                String.format("""
                                                                <p>Votre demande d'affiliation pour le club %s a été rejetée pour la/les raison(s) suivante(s):<br/>%s</p>
                                                                <p>Si vous rencontrez un problème ou si vous avez des questions, n'hésitez pas à nous contacter à l'adresse <a href="mailto:contact@ffsaf.fr">contact@ffsaf.fr</a>.</p>
                                                                """,
                                                        aff.getName(), reason) +
                                                Utils.HTML_FOOTER
                                )
                                .setText(String.format("""
                                        Bonjour,
                                        
                                        Votre demande d'affiliation pour le club %s a été rejetée pour la/les raison(s) suivante(s):
                                        %s
                                        
                                        Si vous rencontrez un problème ou si vous avez des questions, n'hésitez pas à nous contacter à l'adresse contact@ffsaf.fr.
                                        
                                        Cordialement,
                                        L'équipe de la FFSAF
                                        """, aff.getName(), reason))
                                .setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("contact@ffsaf.fr")
                                .addTo(aff.getM2_email(), aff.getM3_email())
                ) : Uni.createFrom().nullItem())
                .chain(aff -> Panache.withTransaction(() -> repositoryRequest.delete(aff)))
                .call(__ -> Utils.deleteMedia(id, media, "aff_request/logo"))
                .call(__ -> Utils.deleteMedia(id, media, "aff_request/status"));
    }
}
