package dev.lions.unionflow.server.service;

import dev.lions.unionflow.server.api.dto.reference.request.CreateTypeReferenceRequest;
import dev.lions.unionflow.server.api.dto.reference.request.UpdateTypeReferenceRequest;
import dev.lions.unionflow.server.api.dto.reference.response.TypeReferenceResponse;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.entity.TypeReference;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import dev.lions.unionflow.server.repository.TypeReferenceRepository;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import java.text.Normalizer;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import org.jboss.logging.Logger;

/**
 * Service de gestion des données de référence.
 *
 * <p>
 * Fournit le CRUD complet sur la table
 * {@code types_reference} avec validation métier,
 * gestion du cache et protection des valeurs
 * système.
 *
 * @author UnionFlow Team
 * @version 3.0
 * @since 2026-02-21
 */
@ApplicationScoped
public class TypeReferenceService {

        private static final Logger LOG = Logger.getLogger(TypeReferenceService.class);

        @Inject
        TypeReferenceRepository repository;

        @Inject
        OrganisationRepository organisationRepository;

        @Inject
        KeycloakService keycloakService;

        /**
         * Liste les références actives d'un domaine.
         *
         * @param domaine        le domaine fonctionnel
         * @param organisationId l'UUID de l'organisation
         *                       (peut être {@code null} pour global)
         * @return liste de réponses triées par ordre
         */
        public List<TypeReferenceResponse> listerParDomaine(
                        String domaine, UUID organisationId) {
                return repository
                                .findByDomaine(domaine, organisationId)
                                .stream()
                                .map(this::toResponse)
                                .collect(Collectors.toList());
        }

        /**
         * Retourne la liste des domaines disponibles.
         *
         * @return noms de domaines distincts
         */
        public List<String> listerDomaines() {
                return repository.listDomaines();
        }

        /**
         * Retourne la valeur par défaut d'un domaine.
         *
         * @param domaine        le domaine fonctionnel
         * @param organisationId l'UUID de l'organisation
         * @return la valeur par défaut
         * @throws IllegalArgumentException si aucune
         *                                  valeur par défaut n'est définie
         */
        public TypeReferenceResponse trouverDefaut(
                        String domaine, UUID organisationId) {
                if (domaine == null || domaine.isBlank()) {
                        throw new IllegalArgumentException("Le paramètre 'domaine' est obligatoire");
                }
                return repository
                                .findDefaut(domaine, organisationId)
                                .map(this::toResponse)
                                .orElseThrow(() -> new IllegalArgumentException(
                                                "Aucune valeur par défaut pour le"
                                                                + " domaine: " + domaine));
        }

        /**
         * Recherche une référence par son identifiant.
         *
         * @param id l'UUID de la référence
         * @return la réponse complète
         * @throws IllegalArgumentException si non trouvée
         */
        public TypeReferenceResponse trouverParId(
                        UUID id) {
                return repository
                                .findByIdOptional(id)
                                .map(this::toResponse)
                                .orElseThrow(() -> new IllegalArgumentException(
                                                "Type de référence introuvable: "
                                                                + id));
        }

        /**
         * Crée une nouvelle donnée de référence.
         *
         * @param request la requête de création
         * @return la réponse avec l'entité créée
         * @throws IllegalArgumentException si le code
         *                                  existe déjà dans le domaine
         */
        @Transactional
        public TypeReferenceResponse creer(
                        CreateTypeReferenceRequest request) {
                // Auto-génération du code depuis le libellé si non fourni
                // ou nettoyage si fourni (UPPER_SNAKE_CASE, sans accents ni caractères spéciaux)
                String code = (request.code() != null && !request.code().isBlank())
                                ? normaliserCode(request.code())
                                : genererCodeDepuisLibelle(request.libelle());

                // Dédoublonnage : si le code existe déjà, suffixer _2, _3, etc.
                code = assurerUniciteCode(code, request.domaine(), request.organisationId());

                validerUnicite(
                                request.domaine(),
                                code,
                                request.organisationId());

                TypeReference entity = TypeReference.builder()
                                .domaine(request.domaine())
                                .code(code)
                                .libelle(request.libelle())
                                .description(request.description())
                                .icone(request.icone())
                                .couleur(request.couleur())
                                .severity(request.severity())
                                .ordreAffichage(
                                                request.ordreAffichage() != null
                                                                ? request.ordreAffichage()
                                                                : 0)
                                .estDefaut(
                                                request.estDefaut() != null
                                                                ? request.estDefaut()
                                                                : false)
                                .estSysteme(
                                                request.estSysteme() != null
                                                                ? request.estSysteme()
                                                                : false)
                                .categorie(request.categorie())
                                .modulesRequis(request.modulesRequis())
                                .build();

                validerLbcFtSiFinancierSolidaire(
                                entity.getCategorie(), entity.getModulesRequis());

                if (request.organisationId() != null) {
                        Organisation org = organisationRepository
                                        .findById(request.organisationId());
                        entity.setOrganisation(org);
                }

                entity.setCreePar(
                                keycloakService.getCurrentUserEmail());
                repository.persist(entity);

                LOG.infof(
                                "Référence créée: %s/%s",
                                entity.getDomaine(),
                                entity.getCode());
                return toResponse(entity);
        }

        /**
         * Met à jour une donnée de référence existante.
         *
         * @param id      l'UUID de la référence
         * @param request la requête de mise à jour
         * @return la réponse mise à jour
         * @throws IllegalArgumentException si non trouvée
         *                                  ou si la valeur est système et le code
         *                                  change
         */
        @Transactional
        public TypeReferenceResponse modifier(
                        UUID id, UpdateTypeReferenceRequest request) {
                TypeReference entity = repository
                                .findByIdOptional(id)
                                .orElseThrow(() -> new IllegalArgumentException(
                                                "Type de référence introuvable: "
                                                                + id));

                if (request.code() != null
                                && !request.code()
                                                .equalsIgnoreCase(entity.getCode())) {
                        if (Boolean.TRUE.equals(entity.getEstSysteme())) {
                                throw new IllegalArgumentException(
                                                "Le code d'une valeur système ne"
                                                                + " peut pas être modifié");
                        }
                        validerUnicite(
                                        entity.getDomaine(),
                                        request.code(),
                                        entity.getOrganisation() != null
                                                        ? entity.getOrganisation().getId()
                                                        : null);
                        entity.setCode(request.code().toUpperCase());
                }

                appliquerMiseAJour(entity, request);
                validerLbcFtSiFinancierSolidaire(
                                entity.getCategorie(), entity.getModulesRequis());
                entity.setModifiePar(
                                keycloakService.getCurrentUserEmail());
                repository.update(entity);

                LOG.infof(
                                "Référence modifiée: %s/%s",
                                entity.getDomaine(),
                                entity.getCode());
                return toResponse(entity);
        }

        /**
         * Supprime une donnée de référence.
         *
         * <p>
         * Les valeurs système ({@code estSysteme=true})
         * ne peuvent pas être supprimées.
         *
         * @param id l'UUID de la référence
         * @throws IllegalArgumentException si non trouvée
         *                                  ou si la valeur est système
         */
        @Transactional
        public void supprimer(UUID id) {
                TypeReference entity = repository
                                .findByIdOptional(id)
                                .orElseThrow(() -> new IllegalArgumentException(
                                                "Type de référence introuvable: "
                                                                + id));

                if (Boolean.TRUE.equals(entity.getEstSysteme())) {
                        throw new IllegalArgumentException(
                                        "Impossible de supprimer une valeur"
                                                        + " système: " + entity.getCode());
                }

                repository.delete(entity);
                LOG.infof(
                                "Référence supprimée: %s/%s",
                                entity.getDomaine(),
                                entity.getCode());
        }

        /**
         * Supprime une donnée de référence même si elle est marquée système.
         * Réservé au rôle SUPER_ADMIN (vérifié par le resource).
         *
         * @param id l'UUID de la référence
         * @throws IllegalArgumentException si non trouvée
         */
        @Transactional
        public void supprimerPourSuperAdmin(UUID id) {
                TypeReference entity = repository
                                .findByIdOptional(id)
                                .orElseThrow(() -> new IllegalArgumentException(
                                                "Type de référence introuvable: "
                                                                + id));
                repository.delete(entity);
                LOG.infof(
                                "Référence supprimée (super admin): %s/%s",
                                entity.getDomaine(),
                                entity.getCode());
        }

        /**
         * Convertit une entité en réponse DTO.
         *
         * @param entity l'entité source
         * @return la réponse complète
         */
        private TypeReferenceResponse toResponse(
                        TypeReference entity) {
                TypeReferenceResponse response = TypeReferenceResponse.builder()
                                .domaine(entity.getDomaine())
                                .code(entity.getCode())
                                .libelle(entity.getLibelle())
                                .description(entity.getDescription())
                                .icone(entity.getIcone())
                                .couleur(entity.getCouleur())
                                .severity(entity.getSeverity())
                                .ordreAffichage(entity.getOrdreAffichage())
                                .estDefaut(entity.getEstDefaut())
                                .estSysteme(entity.getEstSysteme())
                                .organisationId(
                                                entity.getOrganisation() != null
                                                                ? entity.getOrganisation().getId()
                                                                : null)
                                .categorie(entity.getCategorie())
                                .modulesRequis(entity.getModulesRequis())
                                .build();

                response.setId(entity.getId());
                response.setActif(entity.getActif());
                response.setDateCreation(entity.getDateCreation());
                response.setDateModification(entity.getDateModification());
                response.setVersion(entity.getVersion());

                return response;
        }

        /**
         * Applique les champs de mise à jour non nuls.
         *
         * @param entity  l'entité à modifier
         * @param request la requête de mise à jour
         */
        private void appliquerMiseAJour(
                        TypeReference entity,
                        UpdateTypeReferenceRequest request) {
                if (request.libelle() != null) {
                        entity.setLibelle(request.libelle());
                }
                if (request.description() != null) {
                        entity.setDescription(request.description());
                }
                if (request.icone() != null) {
                        entity.setIcone(request.icone());
                }
                if (request.couleur() != null) {
                        entity.setCouleur(request.couleur());
                }
                if (request.severity() != null) {
                        entity.setSeverity(request.severity());
                }
                if (request.ordreAffichage() != null) {
                        entity.setOrdreAffichage(
                                        request.ordreAffichage());
                }
                if (request.estDefaut() != null) {
                        entity.setEstDefaut(request.estDefaut());
                }
                if (request.actif() != null) {
                        entity.setActif(request.actif());
                }
                if (request.categorie() != null) {
                        entity.setCategorie(request.categorie());
                }
                if (request.modulesRequis() != null) {
                        entity.setModulesRequis(request.modulesRequis());
                }
        }

        /**
         * Réglementation BCEAO : LCB_FT obligatoire pour FINANCIER_SOLIDAIRE.
         * Appelé avant persist et avant update pour couvrir les deux flux.
         */
        private void validerLbcFtSiFinancierSolidaire(
                        String categorie, String modulesRequis) {
                if (!"FINANCIER_SOLIDAIRE".equals(categorie)) {
                        return;
                }
                boolean contientLbcFt = modulesRequis != null
                                && Arrays.asList(modulesRequis.split(","))
                                                .contains("LCB_FT");
                if (!contientLbcFt) {
                        throw new IllegalArgumentException(
                                        "Le module LCB_FT (anti-blanchiment) est obligatoire "
                                        + "pour les types d'organisation de catégorie "
                                        + "FINANCIER_SOLIDAIRE (réglementation BCEAO)");
                }
        }

        /**
         * Valide l'unicité du code dans un domaine.
         *
         * @param domaine        le domaine fonctionnel
         * @param code           le code technique
         * @param organisationId l'UUID de l'organisation
         * @throws IllegalArgumentException si le code
         *                                  existe déjà
         */
        private void validerUnicite(
                        String domaine,
                        String code,
                        UUID organisationId) {
                if (repository.existsByDomaineAndCode(
                                domaine, code, organisationId)) {
                        throw new IllegalArgumentException(
                                        "Le code '" + code
                                                        + "' existe déjà dans le domaine '"
                                                        + domaine + "'");
                }
        }

        // ── Auto-génération de codes techniques ─────────────────────────

        /**
         * Génère un code UPPER_SNAKE_CASE depuis un libellé.
         * Ex: "Mon Association Locale" → "MON_ASSOCIATION_LOCALE"
         *     "Église de Dakar" → "EGLISE_DE_DAKAR"
         *     "Mutuelle d'Épargne" → "MUTUELLE_D_EPARGNE"
         */
        static String genererCodeDepuisLibelle(String libelle) {
                if (libelle == null || libelle.isBlank()) {
                        return "TYPE_" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
                }
                return normaliserCode(libelle);
        }

        /**
         * Normalise une chaîne en code technique UPPER_SNAKE_CASE :
         * strip accents, remplace tout non-alphanumérique par _, collapse _, trim _.
         */
        static String normaliserCode(String input) {
                // 1. Décomposer les accents puis les retirer
                String s = Normalizer.normalize(input, Normalizer.Form.NFD)
                                .replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
                // 2. Remplacer tout non-alphanumérique par underscore
                s = s.replaceAll("[^A-Za-z0-9]+", "_");
                // 3. Majuscules
                s = s.toUpperCase();
                // 4. Collapse underscores multiples et trim
                s = s.replaceAll("_+", "_").replaceAll("^_|_$", "");
                // 5. Safety : si vide après nettoyage
                if (s.isEmpty()) {
                        s = "TYPE_" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
                }
                return s;
        }

        /**
         * S'assure que le code est unique dans le domaine. Si le code existe déjà,
         * suffixe avec _2, _3, etc. jusqu'à trouver un code libre.
         */
        private String assurerUniciteCode(String baseCode, String domaine, UUID organisationId) {
                String candidate = baseCode;
                int suffix = 2;
                while (repository.existsByDomaineAndCode(domaine, candidate, organisationId)) {
                        candidate = baseCode + "_" + suffix;
                        suffix++;
                        if (suffix > 100) {
                                // Protection anti-boucle infinie
                                candidate = baseCode + "_" + UUID.randomUUID().toString().substring(0, 4).toUpperCase();
                                break;
                        }
                }
                return candidate;
        }
}
