package fr.titionfire.ffsaf.domain.service;

import fr.titionfire.ffsaf.data.model.ClubModel;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.rest.exception.DInternalError;
import fr.titionfire.ffsaf.utils.*;
import io.quarkus.mailer.Mail;
import io.quarkus.mailer.reactive.ReactiveMailer;
import io.quarkus.runtime.annotations.RegisterForReflection;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import io.vertx.mutiny.core.Vertx;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.RoleScopeResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;

import java.text.Normalizer;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

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

    @Inject
    Keycloak keycloak;

    @Inject
    ClubService clubService;

    @Inject
    MembreService membreService;

    @ConfigProperty(name = "keycloak.realm")
    String realm;

    @ConfigProperty(name = "email.enabled")
    boolean enabled_email;

    @Inject
    ReactiveMailer reactiveMailer;

    @Inject
    Vertx vertx;

    public Uni<String> getGroupFromClub(ClubModel club) {
        if (club.getClubId() == null) {
            LOGGER.infof("Creation of club group %d-%s...", club.getId(), club.getName());
            return vertx.getOrCreateContext().executeBlocking(() -> {
                        GroupRepresentation clubGroup =
                                keycloak.realm(realm).groups().groups().stream().filter(g -> g.getName().equals("club"))
                                        .findAny()
                                        .orElseThrow(() -> new KeycloakException("Fail to fetch group %s".formatted("club")));

                        GroupRepresentation groupRepresentation = new GroupRepresentation();
                        groupRepresentation.setName(club.getId() + "-" + club.getName());

                        try (Response response =
                                     keycloak.realm(realm).groups().group(clubGroup.getId()).subGroup(groupRepresentation)) {
                            if (!response.getStatusInfo().equals(Response.Status.CREATED) && !response.getStatusInfo()
                                    .equals(Response.Status.CONFLICT))
                                throw new KeycloakException(
                                        "Fail to set group parent for club: %s (reason=%s)".formatted(club.getName(),
                                                response.getStatusInfo().getReasonPhrase()));
                        }

                        return keycloak.realm(realm).groups().group(clubGroup.getId()).getSubGroups(0, 1000, true).stream()
                                .filter(g -> g.getName().startsWith(club.getId() + "-")).findAny()
                                .map(GroupRepresentation::getId)
                                .orElseThrow(
                                        () -> new KeycloakException("Fail to fetch group %s*".formatted(club.getId() + "-")));
                    }
            ).call(id -> clubService.setClubId(club.getId(), id));
        }
        return Uni.createFrom().item(club::getClubId);
    }

    public Uni<String> updateGroupFromClub(ClubModel club) {
        if (club.getClubId() == null) {
            return getGroupFromClub(club);
        } else {
            LOGGER.infof("Updating name of club group %d-%s...", club.getId(), club.getName());
            return vertx.getOrCreateContext().executeBlocking(() -> {
                        GroupRepresentation clubGroup =
                                keycloak.realm(realm).groups().groups().stream().filter(g -> g.getName().equals("club"))
                                        .findAny()
                                        .orElseThrow(() -> new KeycloakException("Fail to fetch group %s".formatted("club")));

                        keycloak.realm(realm).groups().group(clubGroup.getId()).getSubGroups(0, 1000, true).stream()
                                .filter(g -> g.getName().startsWith(club.getId() + "-")).findAny()
                                .ifPresent(groupRepresentation -> {
                                    groupRepresentation.setName(club.getId() + "-" + club.getName());
                                    keycloak.realm(realm).groups().group(groupRepresentation.getId())
                                            .update(groupRepresentation);
                                });

                        return club.getClubId();
                    }
            );
        }
    }

    public Uni<String> getUserFromMember(MembreModel membreModel) {
        if (membreModel.getUserId() == null) {
            return Uni.createFrom()
                    .failure(new DInternalError("No keycloak user linked to the user id=" + membreModel.getId()));
        }
        return Uni.createFrom().item(membreModel::getUserId);
    }

    public Uni<String> setClubGroupMembre(MembreModel membreModel, ClubModel club) {
        if (club == null)
            return getUserFromMember(membreModel).chain(
                    userId -> vertx.getOrCreateContext().executeBlocking(() -> {
                        UserResource user = keycloak.realm(realm).users().get(userId);
                        user.groups().stream().filter(g -> g.getPath().startsWith("/club"))
                                .forEach(g -> user.leaveGroup(g.getId()));
                        return "OK";
                    }));
        else
            return getGroupFromClub(club).chain(
                    clubId -> getUserFromMember(membreModel).chain(
                            userId -> vertx.getOrCreateContext().executeBlocking(() -> {
                                UserResource user = keycloak.realm(realm).users().get(userId);
                                user.groups().stream().filter(g -> g.getPath().startsWith("/club"))
                                        .forEach(g -> user.leaveGroup(g.getId()));
                                user.joinGroup(clubId);
                                LOGGER.infof("Set club \"%s\" to user %s (%s)", club.getName(), userId,
                                        user.toRepresentation().getUsername());
                                return "OK";
                            })));
    }

    public Uni<?> setEmail(String userId, String email) {
        return vertx.getOrCreateContext().executeBlocking(() -> {
            UserResource user = keycloak.realm(realm).users().get(userId);
            UserRepresentation user2 = user.toRepresentation();
            String oldEmail = user2.getEmail();
            if (email.equals(user2.getEmail()))
                return null;
            user2.setEmail(email);
            user2.setRequiredActions(List.of(RequiredAction.VERIFY_EMAIL.name()));
            user.update(user2);
            if (enabled_email)
                user.sendVerifyEmail();
            return oldEmail;
        }).call(oldEmail -> oldEmail == null || !enabled_email ? Uni.createFrom().item("") :
                reactiveMailer.send(
                        Mail.withHtml(oldEmail, "FFSAF - Changement de votre adresse email",
                                        String.format(Utils.HTML_HEADER, "FFSAF - Changement de votre adresse email") +
                                                String.format("""
                                                                <p>Suite à la modification de votre adresse email fournie lors de votre (ré)inscription à la FFSAF,<br/>
                                                                    vous allez recevoir dans les prochaines minutes un email de vérification de votre nouvelle adresse sur celle-ci.</p>
                                                                <p>Ancienne adresse email : <span class="highlight">%s</span><br/>Nouvelle adresse email : <span class="highlight">%s</span></p>
                                                                <p>Si vous n'avez pas demandé cette modification, veuillez contacter le support à l'adresse <a href="mailto:support@ffsaf.fr">support@ffsaf.fr</a>.</p>
                                                                """,
                                                        oldEmail, email) +
                                                Utils.HTML_FOOTER
                                )
                                .setText(String.format("""
                                        Bonjour,
                                        
                                        Suite à la modification de votre adresse email fournie lors de votre (ré)inscription à la FFSAF,
                                        vous allez recevoir dans les prochaines minutes un email de vérification de votre nouvelle adresse sur celle-ci.
                                        
                                        Ancienne adresse email : %s
                                        Nouvelle adresse email : %s
                                        
                                        Si vous n'avez pas demandé cette modification, veuillez contacter le support à l'adresse support@ffsaf.fr.
                                        
                                        Cordialement,
                                        L'équipe de la FFSAF
                                        """, oldEmail, email))
                                .setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("support@ffsaf.fr")
                ).onFailure().invoke(e -> LOGGER.error("Fail to send email", e)));
    }

    public Uni<?> setAutoRoleMembre(String id, RoleAsso role, GradeArbitrage gradeArbitrage) {
        List<String> toRemove = new ArrayList<>(List.of("club_president", "club_tresorier", "club_secretaire",
                "club_respo_intra", "asseseur", "arbitre"));
        List<String> toAdd = new ArrayList<>();

        switch (role) {
            case PRESIDENT, VPRESIDENT -> toAdd.add("club_president");
            case TRESORIER, VTRESORIER -> toAdd.add("club_tresorier");
            case SECRETAIRE, VSECRETAIRE -> toAdd.add("club_secretaire");
            case MEMBREBUREAU -> toAdd.add("club_respo_intra");
        }
        switch (gradeArbitrage) {
            case ARBITRE -> toAdd.addAll(List.of("asseseur", "arbitre"));
            case ASSESSEUR -> toAdd.add("asseseur");
        }
        toRemove.removeAll(toAdd);

        return updateRole(id, toAdd, toRemove);
    }

    public Uni<Pair<UserResource, UserCompteState>> fetchCompte(String id) {
        return vertx.getOrCreateContext().executeBlocking(() -> {
            UserResource user = keycloak.realm(realm).users().get(id);
            UserRepresentation user2 = user.toRepresentation();
            return new Pair<>(user, new UserCompteState(user2.isEnabled(), user2.getUsername(),
                    user2.isEmailVerified()));
        });
    }

    public Uni<List<String>> fetchRole(String id) {
        return vertx.getOrCreateContext().executeBlocking(() ->
                keycloak.realm(realm).users().get(id).roles().realmLevel().listEffective().stream()
                        .map(RoleRepresentation::getName).toList());
    }

    public Uni<?> updateRole(String id, List<String> toAdd, List<String> toRemove) {
        return vertx.getOrCreateContext().executeBlocking(() -> {
            RoleScopeResource resource = keycloak.realm(realm).users().get(id).roles().realmLevel();
            List<RoleRepresentation> roles = keycloak.realm(realm).roles().list();
            resource.add(roles.stream().filter(r -> toAdd.contains(r.getName())).toList());
            resource.remove(roles.stream().filter(r -> toRemove.contains(r.getName())).toList());
            return "OK";
        });
    }

    public Uni<String> initCompte(long id) {
        return membreService.getById(id).invoke(Unchecked.consumer(membreModel -> {
            if (membreModel.getUserId() != null)
                throw new KeycloakException("User already linked to the user id=" + id);
            if (membreModel.getEmail() == null)
                throw new KeycloakException("User email is null");
            if (membreModel.getFname() == null || membreModel.getLname() == null)
                throw new KeycloakException("User name is null");
        })).chain(membreModel -> creatUser(membreModel).chain(user -> {
            LOGGER.infof("Set user id %s to membre %s", user.getId(), membreModel.getId());
            return membreService.setUserId(membreModel.getId(), user.getId()).map(__ -> user.getId());
        }));
    }

    private Uni<UserRepresentation> creatUser(MembreModel membreModel) {
        return vertx.getOrCreateContext().executeBlocking(() -> {
                    String login;
                    int i = 1;
                    do {
                        login = makeLogin(membreModel);
                        if (i > 1) {
                            login += i;
                        }
                        i++;
                    } while (!keycloak.realm(realm).users().searchByUsername(login, true).isEmpty());
                    LOGGER.infof("Creation of user %s...", login);

                    UserRepresentation user = new UserRepresentation();
                    user.setUsername(login);
                    user.setFirstName(membreModel.getFname());
                    user.setLastName(membreModel.getLname());
                    user.setEmail(membreModel.getEmail());
                    user.setEnabled(true);

                    try (Response response = keycloak.realm(realm).users().create(user)) {
                        if (!response.getStatusInfo().equals(Response.Status.CREATED) && !response.getStatusInfo()
                                .equals(Response.Status.CONFLICT))
                            throw new KeycloakException("Fail to creat user %s (reason=%s)".formatted(login,
                                    response.getStatusInfo().getReasonPhrase()));
                    }

                    String finalLogin = login;
                    return getUser(login).orElseThrow(
                            () -> new KeycloakException("Fail to fetch user %s".formatted(finalLogin)));
                })
                .invoke(user -> membreModel.setUserId(user.getId()))
                .call(user -> updateRole(user.getId(), List.of("safca_user"), List.of()))
                .call(user -> enabled_email ? reactiveMailer.send(
                        Mail.withHtml(user.getEmail(), "FFSAF - Creation de votre compte sur l'intranet",
                                        String.format(Utils.HTML_HEADER, "Création de votre compte intranet FFSAF") +
                                                String.format(
                                                        """
                                                                <p>Suite &agrave; votre premi&egrave;re inscription <span class="highlight">%s</span> &agrave; la <strong>F&eacute;d&eacute;ration France Soft Armored Fighting (FFSAF)</strong>, votre compte intranet a &eacute;t&eacute; cr&eacute;&eacute;.</p>
                                                                <p>Ce compte vous permettra de :</p>
                                                                <ul>
                                                                    <li>Consulter vos informations personnelles,</li>
                                                                    <li>Vous inscrire aux comp&eacute;titions (bient&ocirc;t disponible),</li>
                                                                    <li>Consulter les r&eacute;sultats des comp&eacute;titions.</li>
                                                                </ul>
                                                                <p>L&rsquo;intranet est accessible &agrave; l&rsquo;adresse suivante :</p>
                                                                <p style="text-align: center;"><a href="https://intra.ffsaf.fr" class="button">Acc&eacute;der &agrave; l&apos;intranet</a></p>
                                                                <p>Votre nom d&rsquo;utilisateur est : <span class="highlight">%s</span></p>
                                                                <p>Pour d&eacute;finir votre mot de passe, rendez-vous sur l&rsquo;intranet : <strong>Connexion &gt; Mot de passe oubli&eacute; ?</strong></p>
                                                                <p>Si vous n&rsquo;avez pas demand&eacute; cette inscription, veuillez contacter le support &agrave; l&rsquo;adresse : <a href="mailto:support@ffsaf.fr">support@ffsaf.fr</a>.</p>
                                                                <p class="footer">(Pas de panique, nous ne vous enverrons pas de message autre que ceux concernant votre compte.)</p>
                                                                """,
                                                        membreModel.getRole() == RoleAsso.MEMBRE ? "par votre club (" + membreModel.getClub()
                                                                .getName() + ") " : "", user.getUsername()) +
                                                Utils.HTML_FOOTER
                                )
                                .setText(String.format(
                                        """
                                                Bonjour,
                                                
                                                Suite à votre première inscription %sà la Fédération France Soft Armored Fighting (FFSAF), votre compte intranet a été créé.
                                                Ce compte vous permettra de consulter vos informations et, dans un futur proche, de vous inscrire aux compétitions ainsi que d'en consulter les résultats.
                                                
                                                L'intranet est accessible à l'adresse suivante : https://intra.ffsaf.fr
                                                Votre nom d'utilisateur est : %s
                                                
                                                Pour définir votre mot de passe, rendez-vous sur l'intranet > "Connexion" > "Mot de passe oublié ?"
                                                
                                                Si vous n'avez pas demandé cette inscription, veuillez contacter le support à l'adresse support@ffsaf.fr.
                                                (Pas de panique, nous ne vous enverrons pas de message autre que ce concernant votre compte)
                                                
                                                Cordialement,
                                                L'équipe de la FFSAF
                                                """,
                                        membreModel.getRole() == RoleAsso.MEMBRE ? "par votre club (" + membreModel.getClub()
                                                .getName() + ") " : "", user.getUsername()))
                                .setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("support@ffsaf.fr")
                ) : Uni.createFrom().nullItem())
                .call(user -> membreService.setUserId(membreModel.getId(), user.getId()))
                .call(user -> setClubGroupMembre(membreModel, membreModel.getClub()));
    }

    public Uni<?> setId(long id, String nid) {
        return membreService.setUserId(id, nid).map(__ -> "OK");
    }

    public Uni<?> removeAccount(String userId) {
        return vertx.getOrCreateContext().executeBlocking(() -> {
            try (Response response = keycloak.realm(realm).users().delete(userId)) {
                System.out.println(response.getStatusInfo());
                if (!response.getStatusInfo().equals(Response.Status.NO_CONTENT))
                    throw new KeycloakException("Fail to delete user %s (reason=%s)".formatted(userId,
                            response.getStatusInfo().getReasonPhrase()));
            }
            return null;
        });
    }

    public Uni<?> removeClubGroup(String clubId) {
        return vertx.getOrCreateContext().executeBlocking(() -> {
            keycloak.realm(realm).groups().group(clubId).remove();
            return null;
        });
    }

    public Uni<?> clearUser(String userId) {
        List<String> toRemove = new ArrayList<>(
                List.of("club_president", "club_tresorier", "club_secretaire", "club_respo_intra"));

        return vertx.getOrCreateContext().executeBlocking(() -> {
            UserResource user = keycloak.realm(realm).users().get(userId);

            RoleScopeResource resource = user.roles().realmLevel();
            List<RoleRepresentation> roles = keycloak.realm(realm).roles().list();
            resource.remove(roles.stream().filter(r -> toRemove.contains(r.getName())).toList());

            user.groups().stream().filter(g -> g.getPath().startsWith("/club"))
                    .forEach(g -> user.leaveGroup(g.getId()));
            return "OK";
        });
    }

    public Optional<UserRepresentation> getUser(String username) {
        List<UserRepresentation> users = keycloak.realm(realm).users().searchByUsername(username, true);

        if (users.isEmpty())
            return Optional.empty();
        else
            return Optional.of(users.get(0));
    }


    public Optional<UserRepresentation> getUser(UUID userId) {
        return getUserById(userId.toString());
    }

    public Optional<UserRepresentation> getUserById(String userId) {
        try {
            UserResource user = keycloak.realm(realm).users().get(userId);
            if (user == null)
                return Optional.empty();
            else
                return Optional.of(user.toRepresentation());
        } catch (Exception e) {
            return Optional.empty();
        }
    }

    private String makeLogin(MembreModel model) {
        return Normalizer.normalize(
                        (model.getFname().toLowerCase() + "." + model.getLname().toLowerCase()).replace(' ', '_'),
                        Normalizer.Form.NFD)
                .replaceAll("\\p{M}", "");

    }

    @RegisterForReflection
    public record UserCompteState(Boolean enabled, String login, Boolean emailVerified) {
    }
}
