package dev.lions.user.manager.service.impl;

import dev.lions.user.manager.dto.audit.AuditLogDTO;
import dev.lions.user.manager.enums.audit.TypeActionAudit;
import dev.lions.user.manager.server.impl.entity.AuditLogEntity;
import dev.lions.user.manager.server.impl.mapper.AuditLogMapper;
import dev.lions.user.manager.service.AuditService;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.microprofile.config.inject.ConfigProperty;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

/**
 * Implémentation du service d'audit avec support de la persistance PostgreSQL.
 *
 * <p><b>Architecture Hybride:</b></p>
 * <ul>
 *   <li><b>Cache en mémoire</b> - Pour les logs récents (performances)</li>
 *   <li><b>Persistance PostgreSQL</b> - Pour l'historique long terme (activable via config)</li>
 * </ul>
 *
 * <p><b>Configuration:</b></p>
 * <ul>
 *   <li>{@code lions.audit.enabled} - Active/désactive l'audit (défaut: true)</li>
 *   <li>{@code lions.audit.log-to-database} - Active la persistance DB (défaut: false en dev, true en prod)</li>
 *   <li>{@code lions.audit.cache-size} - Taille max du cache mémoire (défaut: 10000)</li>
 *   <li>{@code lions.audit.retention-days} - Durée de rétention en jours (défaut: 365)</li>
 * </ul>
 *
 * <p><b>Modes de Fonctionnement:</b></p>
 * <pre>
 * Mode DEV (logToDatabase=false):
 *   - Stockage en mémoire uniquement
 *   - Logs perdus au redémarrage
 *   - Performances maximales
 *
 * Mode PROD (logToDatabase=true):
 *   - Persistance PostgreSQL
 *   - Cache mémoire pour requêtes fréquentes
 *   - Historique complet préservé
 * </pre>
 *
 * @author Lions Development Team
 * @version 2.0.0
 * @since 2026-01-02
 */
@ApplicationScoped
@Slf4j
public class AuditServiceImpl implements AuditService {

    // ==================== DÉPENDANCES ====================

    @Inject
    AuditLogMapper auditLogMapper;

    // ==================== CONFIGURATION ====================

    @ConfigProperty(name = "lions.audit.enabled", defaultValue = "true")
    boolean auditEnabled;

    @ConfigProperty(name = "lions.audit.log-to-database", defaultValue = "false")
    boolean logToDatabase;

    @ConfigProperty(name = "lions.audit.cache-size", defaultValue = "10000")
    int cacheSize;

    @ConfigProperty(name = "lions.audit.retention-days", defaultValue = "365")
    int retentionDays;

    // ==================== STOCKAGE ====================

    /**
     * Cache en mémoire pour les logs récents.
     * <p>Limité à {@code cacheSize} entrées. Les plus anciens sont supprimés automatiquement.</p>
     */
    private final Map<String, AuditLogDTO> auditLogsCache = new ConcurrentHashMap<>();

    // ==================== MÉTHODES PRINCIPALES ====================

    @Override
    @Transactional
    public AuditLogDTO logAction(@Valid @NotNull AuditLogDTO auditLog) {
        if (!auditEnabled) {
            log.debug("Audit désactivé, log ignoré");
            return auditLog;
        }

        // Générer un ID si nécessaire
        if (auditLog.getId() == null) {
            auditLog.setId(UUID.randomUUID().toString());
        }

        // Ajouter le timestamp si nécessaire
        if (auditLog.getDateAction() == null) {
            auditLog.setDateAction(LocalDateTime.now());
        }

        // Log structuré pour les systèmes de logging externes (Graylog, Elasticsearch, etc.)
        log.info("AUDIT | Type: {} | Acteur: {} | Ressource: {} | Succès: {} | IP: {} | Détails: {}",
                 auditLog.getTypeAction(),
                 auditLog.getActeurUsername(),
                 auditLog.getRessourceType() + ":" + auditLog.getRessourceId(),
                 auditLog.isSuccessful(),
                 auditLog.getIpAddress(),
                 auditLog.getDescription());

        // Stocker en base de données si activé
        if (logToDatabase) {
            try {
                AuditLogEntity entity = auditLogMapper.toEntity(auditLog);
                // Le mapper s'occupe du mapping automatique via @Mapping annotations
                // Ajout des champs additionnels non mappés automatiquement
                entity.setRealmName(auditLog.getRealmName());

                entity.persist();

                log.debug("Log d'audit persisté en base de données avec ID: {}", entity.id);
            } catch (Exception e) {
                log.error("Erreur lors de la persistance du log d'audit en base de données", e);
                // On ne lance pas d'exception pour ne pas bloquer le processus métier
            }
        }

        // Ajouter au cache mémoire (pour performances)
        auditLogsCache.put(auditLog.getId(), auditLog);

        // Nettoyer le cache si trop grand
        if (auditLogsCache.size() > cacheSize) {
            cleanOldestCacheEntries();
        }

        return auditLog;
    }

    @Override
    public void logSuccess(@NotNull TypeActionAudit typeAction,
                           @NotBlank String ressourceType,
                           String ressourceId,
                           String ressourceName,
                           @NotBlank String realmName,
                           @NotBlank String acteurUserId,
                           String description) {
        AuditLogDTO auditLog = AuditLogDTO.builder()
            .acteurUserId(acteurUserId)
            .acteurUsername(acteurUserId)
            .typeAction(typeAction)
            .ressourceType(ressourceType)
            .ressourceId(ressourceId != null ? ressourceId : "")
            .success(true)
            .description(description)
            .dateAction(LocalDateTime.now())
            .build();

        logAction(auditLog);
    }

    @Override
    public void logFailure(@NotNull TypeActionAudit typeAction,
                           @NotBlank String ressourceType,
                           String ressourceId,
                           String ressourceName,
                           @NotBlank String realmName,
                           @NotBlank String acteurUserId,
                           String errorCode,
                           String errorMessage) {
        AuditLogDTO auditLog = AuditLogDTO.builder()
            .acteurUserId(acteurUserId)
            .acteurUsername(acteurUserId)
            .typeAction(typeAction)
            .ressourceType(ressourceType)
            .ressourceId(ressourceId != null ? ressourceId : "")
            .success(false)
            .errorMessage(errorMessage)
            .dateAction(LocalDateTime.now())
            .build();

        logAction(auditLog);
    }

    // ==================== MÉTHODES DE RECHERCHE ====================

    @Override
    public List<AuditLogDTO> findByActeur(@NotBlank String acteurUserId,
                                          LocalDateTime dateDebut,
                                          LocalDateTime dateFin,
                                          int page,
                                          int pageSize) {
        if (logToDatabase) {
            return searchLogsFromDatabase(acteurUserId, dateDebut, dateFin, null, null, null, page, pageSize);
        }
        return searchLogsFromCache(acteurUserId, dateDebut, dateFin, null, null, null, page, pageSize);
    }

    @Override
    public List<AuditLogDTO> findByRessource(@NotBlank String ressourceType,
                                            @NotBlank String ressourceId,
                                            LocalDateTime dateDebut,
                                            LocalDateTime dateFin,
                                            int page,
                                            int pageSize) {
        if (logToDatabase) {
            return searchLogsFromDatabase(null, dateDebut, dateFin, null, ressourceType, null, page, pageSize)
                .stream()
                .filter(log -> ressourceId.equals(log.getRessourceId()))
                .collect(Collectors.toList());
        }
        return searchLogsFromCache(null, dateDebut, dateFin, null, ressourceType, null, page, pageSize)
            .stream()
            .filter(log -> ressourceId.equals(log.getRessourceId()))
            .collect(Collectors.toList());
    }

    @Override
    public List<AuditLogDTO> findByTypeAction(@NotNull TypeActionAudit typeAction,
                                             @NotBlank String realmName,
                                             LocalDateTime dateDebut,
                                             LocalDateTime dateFin,
                                             int page,
                                             int pageSize) {
        if (logToDatabase) {
            return searchLogsFromDatabase(null, dateDebut, dateFin, typeAction, null, null, page, pageSize);
        }
        return searchLogsFromCache(null, dateDebut, dateFin, typeAction, null, null, page, pageSize);
    }

    @Override
    public List<AuditLogDTO> findByRealm(@NotBlank String realmName,
                                        LocalDateTime dateDebut,
                                        LocalDateTime dateFin,
                                        int page,
                                        int pageSize) {
        if (logToDatabase) {
            List<AuditLogEntity> entities = AuditLogEntity.findByRealm(realmName);
            return auditLogMapper.toDTOList(entities);
        }
        return searchLogsFromCache(null, dateDebut, dateFin, null, null, null, page, pageSize);
    }

    @Override
    public List<AuditLogDTO> findFailures(@NotBlank String realmName,
                                         LocalDateTime dateDebut,
                                         LocalDateTime dateFin,
                                         int page,
                                         int pageSize) {
        if (logToDatabase) {
            return searchLogsFromDatabase(null, dateDebut, dateFin, null, null, false, page, pageSize);
        }
        return searchLogsFromCache(null, dateDebut, dateFin, null, null, false, page, pageSize);
    }

    @Override
    public List<AuditLogDTO> findCriticalActions(@NotBlank String realmName,
                                                  LocalDateTime dateDebut,
                                                  LocalDateTime dateFin,
                                                  int page,
                                                  int pageSize) {
        List<AuditLogDTO> allLogs = logToDatabase ?
            searchLogsFromDatabase(null, dateDebut, dateFin, null, null, null, page, pageSize) :
            searchLogsFromCache(null, dateDebut, dateFin, null, null, null, page, pageSize);

        return allLogs.stream()
            .filter(log -> {
                TypeActionAudit type = log.getTypeAction();
                return type == TypeActionAudit.USER_DELETE ||
                       type == TypeActionAudit.ROLE_DELETE ||
                       type == TypeActionAudit.SESSION_REVOKE_ALL;
            })
            .collect(Collectors.toList());
    }

    // ==================== MÉTHODES STATISTIQUES ====================

    @Override
    public Map<TypeActionAudit, Long> countByActionType(@NotBlank String realmName,
                                                         LocalDateTime dateDebut,
                                                         LocalDateTime dateFin) {
        return getActionStatistics(dateDebut, dateFin);
    }

    @Override
    public Map<String, Long> countByActeur(@NotBlank String realmName,
                                           LocalDateTime dateDebut,
                                           LocalDateTime dateFin) {
        return getUserActivityStatistics(dateDebut, dateFin);
    }

    @Override
    public Map<String, Long> countSuccessVsFailure(@NotBlank String realmName,
                                                    LocalDateTime dateDebut,
                                                    LocalDateTime dateFin) {
        long successCount = getSuccessCount(dateDebut, dateFin);
        long failureCount = getFailureCount(dateDebut, dateFin);

        Map<String, Long> result = new java.util.HashMap<>();
        result.put("success", successCount);
        result.put("failure", failureCount);
        return result;
    }

    @Override
    public Map<String, Object> getAuditStatistics(@NotBlank String realmName,
                                                  LocalDateTime dateDebut,
                                                  LocalDateTime dateFin) {
        Map<String, Object> stats = new java.util.HashMap<>();

        long total = logToDatabase ?
            AuditLogEntity.findByPeriod(dateDebut, dateFin).size() :
            auditLogsCache.values().stream()
                .filter(log -> {
                    if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
                        return false;
                    }
                    if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
                        return false;
                    }
                    return true;
                })
                .count();

        stats.put("total", total);
        stats.put("success", getSuccessCount(dateDebut, dateFin));
        stats.put("failure", getFailureCount(dateDebut, dateFin));
        stats.put("byActionType", countByActionType(realmName, dateDebut, dateFin));
        stats.put("byActeur", countByActeur(realmName, dateDebut, dateFin));
        return stats;
    }

    // ==================== EXPORT / PURGE ====================

    @Override
    public String exportToCSV(@NotBlank String realmName,
                              LocalDateTime dateDebut,
                              LocalDateTime dateFin) {
        List<String> csvLines = exportLogsToCSV(dateDebut, dateFin);
        return String.join("\n", csvLines);
    }

    @Override
    @Transactional
    public long purgeOldLogs(@NotNull LocalDateTime dateLimite) {
        long purgedCount = 0;

        // Purge en base de données si activé
        if (logToDatabase) {
            purgedCount = AuditLogEntity.deleteOlderThan(dateLimite);
            log.info("Supprimé {} logs d'audit de la base de données avant {}", purgedCount, dateLimite);
        }

        // Purge du cache mémoire
        long beforeCacheCount = auditLogsCache.size();
        auditLogsCache.entrySet().removeIf(entry ->
            entry.getValue().getDateAction().isBefore(dateLimite)
        );
        long cacheRemoved = beforeCacheCount - auditLogsCache.size();

        log.info("Supprimé {} logs du cache mémoire avant {}", cacheRemoved, dateLimite);

        return purgedCount + cacheRemoved;
    }

    // ==================== MÉTHODES PRIVÉES ====================

    /**
     * Recherche les logs depuis le cache mémoire.
     */
    private List<AuditLogDTO> searchLogsFromCache(String acteurUsername, LocalDateTime dateDebut,
                                                   LocalDateTime dateFin, TypeActionAudit typeAction,
                                                   String ressourceType, Boolean succes,
                                                   int page, int pageSize) {
        log.debug("Recherche logs depuis cache mémoire");

        return auditLogsCache.values().stream()
            .filter(log -> applyFilters(log, acteurUsername, dateDebut, dateFin, typeAction, ressourceType, succes))
            .sorted((a, b) -> b.getDateAction().compareTo(a.getDateAction()))
            .skip((long) page * pageSize)
            .limit(pageSize)
            .collect(Collectors.toList());
    }

    /**
     * Recherche les logs depuis la base de données PostgreSQL.
     */
    private List<AuditLogDTO> searchLogsFromDatabase(String acteurUsername, LocalDateTime dateDebut,
                                                      LocalDateTime dateFin, TypeActionAudit typeAction,
                                                      String ressourceType, Boolean succes,
                                                      int page, int pageSize) {
        log.debug("Recherche logs depuis base de données");

        List<AuditLogEntity> entities;

        // Optimisation: utiliser les requêtes spécialisées si possible
        if (acteurUsername != null && typeAction == null && ressourceType == null) {
            entities = AuditLogEntity.findByAuteur(acteurUsername);
        } else if (typeAction != null && acteurUsername == null && ressourceType == null) {
            entities = AuditLogEntity.findByAction(typeAction);
        } else if (dateDebut != null && dateFin != null) {
            entities = AuditLogEntity.findByPeriod(dateDebut, dateFin);
        } else {
            entities = AuditLogEntity.listAll();
        }

        return entities.stream()
            .map(auditLogMapper::toDTO)
            .filter(log -> applyFilters(log, acteurUsername, dateDebut, dateFin, typeAction, ressourceType, succes))
            .sorted((a, b) -> b.getDateAction().compareTo(a.getDateAction()))
            .skip((long) page * pageSize)
            .limit(pageSize)
            .collect(Collectors.toList());
    }

    /**
     * Applique les filtres de recherche à un log.
     */
    private boolean applyFilters(AuditLogDTO log, String acteurUsername, LocalDateTime dateDebut,
                                 LocalDateTime dateFin, TypeActionAudit typeAction,
                                 String ressourceType, Boolean succes) {
        if (acteurUsername != null && !"*".equals(acteurUsername) &&
            !acteurUsername.equals(log.getActeurUsername())) {
            return false;
        }

        if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
            return false;
        }

        if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
            return false;
        }

        if (typeAction != null && !typeAction.equals(log.getTypeAction())) {
            return false;
        }

        if (ressourceType != null && !ressourceType.equals(log.getRessourceType())) {
            return false;
        }

        if (succes != null && succes != log.isSuccessful()) {
            return false;
        }

        return true;
    }

    /**
     * Nettoie les entrées les plus anciennes du cache.
     */
    private void cleanOldestCacheEntries() {
        int toRemove = auditLogsCache.size() - (cacheSize * 90 / 100); // Garder 90%

        if (toRemove > 0) {
            List<String> oldestKeys = auditLogsCache.entrySet().stream()
                .sorted((a, b) -> a.getValue().getDateAction().compareTo(b.getValue().getDateAction()))
                .limit(toRemove)
                .map(Map.Entry::getKey)
                .collect(Collectors.toList());

            oldestKeys.forEach(auditLogsCache::remove);
            log.debug("Nettoyé {} entrées du cache d'audit", oldestKeys.size());
        }
    }

    // Méthodes helpers (statistiques)
    private Map<TypeActionAudit, Long> getActionStatistics(LocalDateTime dateDebut, LocalDateTime dateFin) {
        if (logToDatabase) {
            List<AuditLogEntity> entities = AuditLogEntity.findByPeriod(dateDebut, dateFin);
            return entities.stream()
                .collect(Collectors.groupingBy(AuditLogEntity::getAction, Collectors.counting()));
        }

        return auditLogsCache.values().stream()
            .filter(log -> {
                if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) return false;
                if (dateFin != null && log.getDateAction().isAfter(dateFin)) return false;
                return true;
            })
            .collect(Collectors.groupingBy(AuditLogDTO::getTypeAction, Collectors.counting()));
    }

    private Map<String, Long> getUserActivityStatistics(LocalDateTime dateDebut, LocalDateTime dateFin) {
        if (logToDatabase) {
            List<AuditLogEntity> entities = AuditLogEntity.findByPeriod(dateDebut, dateFin);
            return entities.stream()
                .collect(Collectors.groupingBy(AuditLogEntity::getAuteurAction, Collectors.counting()));
        }

        return auditLogsCache.values().stream()
            .filter(log -> {
                if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) return false;
                if (dateFin != null && log.getDateAction().isAfter(dateFin)) return false;
                return true;
            })
            .collect(Collectors.groupingBy(AuditLogDTO::getActeurUsername, Collectors.counting()));
    }

    private long getFailureCount(LocalDateTime dateDebut, LocalDateTime dateFin) {
        if (logToDatabase) {
            return AuditLogEntity.findByPeriod(dateDebut, dateFin).stream()
                .filter(e -> !e.getSuccess())
                .count();
        }

        return auditLogsCache.values().stream()
            .filter(log -> !log.isSuccessful())
            .filter(log -> {
                if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) return false;
                if (dateFin != null && log.getDateAction().isAfter(dateFin)) return false;
                return true;
            })
            .count();
    }

    private long getSuccessCount(LocalDateTime dateDebut, LocalDateTime dateFin) {
        if (logToDatabase) {
            return AuditLogEntity.findByPeriod(dateDebut, dateFin).stream()
                .filter(AuditLogEntity::getSuccess)
                .count();
        }

        return auditLogsCache.values().stream()
            .filter(AuditLogDTO::isSuccessful)
            .filter(log -> {
                if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) return false;
                if (dateFin != null && log.getDateAction().isAfter(dateFin)) return false;
                return true;
            })
            .count();
    }

    private List<String> exportLogsToCSV(LocalDateTime dateDebut, LocalDateTime dateFin) {
        log.info("Export CSV des logs d'audit entre {} et {}", dateDebut, dateFin);

        List<String> csvLines = new ArrayList<>();
        csvLines.add("ID,Date Action,Acteur,Type Action,Ressource Type,Ressource ID,Succès,Adresse IP,Détails,Message Erreur");

        List<AuditLogDTO> logs;
        if (logToDatabase) {
            List<AuditLogEntity> entities = AuditLogEntity.findByPeriod(dateDebut, dateFin);
            logs = auditLogMapper.toDTOList(entities);
        } else {
            logs = auditLogsCache.values().stream()
                .filter(log -> {
                    if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) return false;
                    if (dateFin != null && log.getDateAction().isAfter(dateFin)) return false;
                    return true;
                })
                .sorted((a, b) -> a.getDateAction().compareTo(b.getDateAction()))
                .collect(Collectors.toList());
        }

        logs.forEach(log -> {
            String csvLine = String.format("%s,%s,%s,%s,%s,%s,%s,%s,\"%s\",\"%s\"",
                log.getId(),
                log.getDateAction(),
                log.getActeurUsername(),
                log.getTypeAction(),
                log.getRessourceType(),
                log.getRessourceId(),
                log.isSuccessful(),
                log.getIpAddress() != null ? log.getIpAddress() : "",
                log.getDescription() != null ? log.getDescription().replace("\"", "\"\"") : "",
                log.getErrorMessage() != null ? log.getErrorMessage().replace("\"", "\"\"") : ""
            );
            csvLines.add(csvLine);
        });

        log.info("Export CSV terminé: {} lignes", csvLines.size() - 1);
        return csvLines;
    }

    // ==================== MÉTHODES UTILITAIRES ====================

    /**
     * Retourne le nombre total de logs (cache + DB).
     */
    public long getTotalCount() {
        if (logToDatabase) {
            return AuditLogEntity.count();
        }
        return auditLogsCache.size();
    }

    /**
     * Vide tous les logs (ATTENTION: à utiliser uniquement en développement).
     */
    @Transactional
    public void clearAll() {
        log.warn("ATTENTION: Suppression de tous les logs d'audit");

        if (logToDatabase) {
            AuditLogEntity.deleteAll();
            log.warn("Supprimé tous les logs de la base de données");
        }

        auditLogsCache.clear();
        log.warn("Vidé le cache mémoire");
    }
}
