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

import dev.lions.user.manager.server.impl.entity.SyncHistoryEntity;
import dev.lions.user.manager.server.impl.entity.SyncedRoleEntity;
import dev.lions.user.manager.server.impl.entity.SyncedUserEntity;
import dev.lions.user.manager.server.impl.interceptor.Logged;
import dev.lions.user.manager.server.impl.repository.SyncHistoryRepository;
import dev.lions.user.manager.server.impl.repository.SyncedRoleRepository;
import dev.lions.user.manager.server.impl.repository.SyncedUserRepository;
import dev.lions.user.manager.service.SyncService;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.validation.constraints.NotBlank;
import lombok.extern.slf4j.Slf4j;
import dev.lions.user.manager.client.KeycloakAdminClient;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

@ApplicationScoped
@Slf4j
public class SyncServiceImpl implements SyncService {

    @Inject
    Keycloak keycloak;

    @Inject
    KeycloakAdminClient keycloakAdminClient;

    @Inject
    SyncHistoryRepository syncHistoryRepository;

    // Repositories optionnels pour la persistance locale des snapshots.
    // Ils sont marqués @Inject mais l'utilisation dans le code est protégée
    // par des checks null pour ne pas casser les tests existants.
    @Inject
    SyncedUserRepository syncedUserRepository;

    @Inject
    SyncedRoleRepository syncedRoleRepository;

    @ConfigProperty(name = "lions.keycloak.server-url")
    String keycloakServerUrl;

    @Override
    @Transactional
    @Logged(action = "SYNC_USERS", resource = "REALM")
    public int syncUsersFromRealm(@NotBlank String realmName) {
        log.info("Synchronisation des utilisateurs depuis le realm: {}", realmName);
        LocalDateTime start = LocalDateTime.now();
        int count = 0;
        String status = "SUCCESS";
        String errorMessage = null;

        try {
            List<UserRepresentation> users = keycloak.realm(realmName).users().list();
            count = users.size();

            // Persister un snapshot minimal des utilisateurs dans la base locale si le
            // repository est disponible.
            if (syncedUserRepository != null && !users.isEmpty()) {
                List<SyncedUserEntity> snapshots = users.stream()
                        .map(user -> {
                            SyncedUserEntity entity = new SyncedUserEntity();
                            entity.setRealmName(realmName);
                            entity.setKeycloakId(user.getId());
                            entity.setUsername(user.getUsername());
                            entity.setEmail(user.getEmail());
                            entity.setEnabled(user.isEnabled());
                            entity.setEmailVerified(user.isEmailVerified());

                            if (user.getCreatedTimestamp() != null) {
                                LocalDateTime createdAt = LocalDateTime.ofInstant(
                                        Instant.ofEpochMilli(user.getCreatedTimestamp()),
                                        ZoneOffset.UTC);
                                entity.setCreatedAt(createdAt);
                            }
                            return entity;
                        })
                        .toList();

                syncedUserRepository.replaceForRealm(realmName, snapshots);
                log.info("Persisted {} synced user snapshots for realm {}", snapshots.size(), realmName);
            }

            log.info("✅ {} utilisateurs synchronisés depuis le realm {}", count, realmName);
        } catch (Exception e) {
            log.error("❌ Erreur lors de la synchronisation des utilisateurs depuis le realm {}", realmName, e);
            status = "FAILURE";
            errorMessage = e.getMessage();
            throw new RuntimeException("Erreur de synchronisation utilisateurs: " + e.getMessage(), e);
        } finally {
            recordSyncHistory(realmName, "USER", status, count, start, errorMessage);
        }
        return count;
    }

    @Override
    @Transactional
    @Logged(action = "SYNC_ROLES", resource = "REALM")
    public int syncRolesFromRealm(@NotBlank String realmName) {
        log.info("Synchronisation des rôles depuis le realm: {}", realmName);
        LocalDateTime start = LocalDateTime.now();
        int count = 0;
        String status = "SUCCESS";
        String errorMessage = null;

        try {
            List<RoleRepresentation> roles = keycloak.realm(realmName).roles().list();
            count = roles.size();

            // Persister un snapshot minimal des rôles dans la base locale si le repository
            // est disponible.
            if (syncedRoleRepository != null && !roles.isEmpty()) {
                List<SyncedRoleEntity> snapshots = roles.stream()
                        .map(role -> {
                            SyncedRoleEntity entity = new SyncedRoleEntity();
                            entity.setRealmName(realmName);
                            entity.setRoleName(role.getName());
                            entity.setDescription(role.getDescription());
                            return entity;
                        })
                        .toList();

                syncedRoleRepository.replaceForRealm(realmName, snapshots);
                log.info("Persisted {} synced role snapshots for realm {}", snapshots.size(), realmName);
            }

            log.info("✅ {} rôles synchronisés depuis le realm {}", count, realmName);
        } catch (Exception e) {
            log.error("❌ Erreur lors de la synchronisation des rôles depuis le realm {}", realmName, e);
            status = "FAILURE";
            errorMessage = e.getMessage();
            throw new RuntimeException("Erreur de synchronisation rôles: " + e.getMessage(), e);
        } finally {
            recordSyncHistory(realmName, "ROLE", status, count, start, errorMessage);
        }
        return count;
    }

    @Override
    @Transactional
    @Logged(action = "REALM_SYNC", resource = "SYSTEM")
    public Map<String, Integer> syncAllRealms() {
        Map<String, Integer> result = new HashMap<>();
        try {
            // getAllRealms() utilise un HttpClient raw avec ObjectMapper(FAIL_ON_UNKNOWN_PROPERTIES=false)
            // pour éviter les erreurs de désérialisation de RealmRepresentation avec Keycloak 26+
            List<String> realmNames = keycloakAdminClient.getAllRealms();

            for (String realmName : realmNames) {
                if (realmName == null || realmName.isBlank()) {
                    continue;
                }

                log.info("Synchronisation complète du realm {}", realmName);
                int totalForRealm = 0;
                try {
                    int users = syncUsersFromRealm(realmName);
                    int roles = syncRolesFromRealm(realmName);
                    totalForRealm = users + roles;
                    log.info("✅ Realm {} synchronisé (users={}, roles={})", realmName, users, roles);
                } catch (Exception e) {
                    log.error("❌ Erreur lors de la synchronisation du realm {}", realmName, e);
                    // On enregistre quand même le realm dans le résultat avec 0 éléments traités
                    totalForRealm = 0;
                }
                result.put(realmName, totalForRealm);
            }
        } catch (Exception e) {
            log.error("❌ Erreur lors de la récupération de la liste des realms pour synchronisation globale", e);
            // En cas d'erreur globale, on retourne simplement une map vide (aucune
            // approximation)
        }
        return result;
    }

    @Override
    public Map<String, Object> checkDataConsistency(@NotBlank String realmName) {
        Map<String, Object> report = new HashMap<>();
        report.put("realmName", realmName);

        try {
            // Données actuelles dans Keycloak
            List<UserRepresentation> kcUsers = keycloak.realm(realmName).users().list();
            List<RoleRepresentation> kcRoles = keycloak.realm(realmName).roles().list();

            // Snapshots locaux
            List<SyncedUserEntity> localUsers = syncedUserRepository.list("realmName", realmName);
            List<SyncedRoleEntity> localRoles = syncedRoleRepository.list("realmName", realmName);

            // Comparaison exacte des identifiants utilisateurs
            Set<String> kcUserIds = kcUsers.stream()
                    .map(UserRepresentation::getId)
                    .filter(id -> id != null && !id.isBlank())
                    .collect(java.util.stream.Collectors.toSet());

            Set<String> localUserIds = localUsers.stream()
                    .map(SyncedUserEntity::getKeycloakId)
                    .filter(id -> id != null && !id.isBlank())
                    .collect(java.util.stream.Collectors.toSet());

            Set<String> missingUsersInLocal = new HashSet<>(kcUserIds);
            missingUsersInLocal.removeAll(localUserIds);

            Set<String> missingUsersInKeycloak = new HashSet<>(localUserIds);
            missingUsersInKeycloak.removeAll(kcUserIds);

            // Comparaison exacte des noms de rôles
            Set<String> kcRoleNames = kcRoles.stream()
                    .map(RoleRepresentation::getName)
                    .filter(name -> name != null && !name.isBlank())
                    .collect(java.util.stream.Collectors.toSet());

            Set<String> localRoleNames = localRoles.stream()
                    .map(SyncedRoleEntity::getRoleName)
                    .filter(name -> name != null && !name.isBlank())
                    .collect(java.util.stream.Collectors.toSet());

            Set<String> missingRolesInLocal = new HashSet<>(kcRoleNames);
            missingRolesInLocal.removeAll(localRoleNames);

            Set<String> missingRolesInKeycloak = new HashSet<>(localRoleNames);
            missingRolesInKeycloak.removeAll(kcRoleNames);

            boolean usersOk = missingUsersInLocal.isEmpty() && missingUsersInKeycloak.isEmpty();
            boolean rolesOk = missingRolesInLocal.isEmpty() && missingRolesInKeycloak.isEmpty();

            report.put("status", (usersOk && rolesOk) ? "OK" : "MISMATCH");

            report.put("usersKeycloakCount", kcUserIds.size());
            report.put("usersLocalCount", localUserIds.size());
            report.put("missingUsersInLocal", missingUsersInLocal);
            report.put("missingUsersInKeycloak", missingUsersInKeycloak);

            report.put("rolesKeycloakCount", kcRoleNames.size());
            report.put("rolesLocalCount", localRoleNames.size());
            report.put("missingRolesInLocal", missingRolesInLocal);
            report.put("missingRolesInKeycloak", missingRolesInKeycloak);
        } catch (Exception e) {
            log.error("❌ Erreur lors du contrôle de cohérence des données pour le realm {}", realmName, e);
            report.put("status", "ERROR");
            report.put("error", e.getMessage());
        }

        return report;
    }

    @Override
    @Transactional
    public Map<String, Object> forceSyncRealm(@NotBlank String realmName) {
        Map<String, Object> result = new HashMap<>();
        try {
            int users = syncUsersFromRealm(realmName);
            int roles = syncRolesFromRealm(realmName);
            result.put("usersSynced", users);
            result.put("rolesSynced", roles);
            result.put("status", "SUCCESS");
        } catch (Exception e) {
            result.put("status", "FAILURE");
            result.put("error", e.getMessage());
        }
        return result;
    }

    @Override
    public Map<String, Object> getLastSyncStatus(@NotBlank String realmName) {
        List<SyncHistoryEntity> history = syncHistoryRepository.findLatestByRealm(realmName, 1);
        if (history.isEmpty()) {
            return Collections.singletonMap("status", "NEVER_SYNCED");
        }
        SyncHistoryEntity lastSync = history.get(0);

        Map<String, Object> statusMap = new HashMap<>(); // Utilisation de HashMap pour permettre nulls si besoin
        statusMap.put("lastSyncDate", lastSync.getSyncDate());
        statusMap.put("status", lastSync.getStatus());
        statusMap.put("type", lastSync.getSyncType());
        statusMap.put("itemsProcessed", lastSync.getItemsProcessed());
        return statusMap;
    }

    @Override
    public boolean isKeycloakAvailable() {
        try {
            // getAllRealms() utilise un HttpClient raw : pas de désérialisation de RealmRepresentation
            // donc pas d'erreur UnrecognizedPropertyException avec Keycloak 26+
            keycloakAdminClient.getAllRealms();
            return true;
        } catch (Exception e) {
            log.warn("Keycloak availability check failed: {}", e.getMessage());
            return false;
        }
    }

    @Override
    public Map<String, Object> getKeycloakHealthInfo() {
        Map<String, Object> health = new HashMap<>();
        try {
            var info = keycloak.serverInfo().getInfo();
            health.put("status", "UP");
            health.put("version", info.getSystemInfo().getVersion());
            health.put("serverTime", info.getSystemInfo().getServerTime());
        } catch (Exception e) {
            log.debug("serverInfo().getInfo() failed, trying raw HTTP fallback: {}", e.getMessage());
            fetchVersionViaHttp(health);
        }
        return health;
    }

    private void fetchVersionViaHttp(Map<String, Object> health) {
        try {
            String token = keycloak.tokenManager().getAccessTokenString();
            var client = java.net.http.HttpClient.newHttpClient();
            var request = java.net.http.HttpRequest.newBuilder()
                    .uri(java.net.URI.create(keycloakServerUrl + "/admin/serverinfo"))
                    .header("Authorization", "Bearer " + token)
                    .header("Accept", "application/json")
                    .GET().build();
            var response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString());
            if (response.statusCode() == 200) {
                String body = response.body();
                health.put("status", "UP");
                int sysInfoIdx = body.indexOf("\"systemInfo\"");
                if (sysInfoIdx >= 0) {
                    extractJsonStringField(body, "version", sysInfoIdx)
                            .ifPresent(v -> health.put("version", v));
                    extractJsonStringField(body, "serverTime", sysInfoIdx)
                            .ifPresent(v -> health.put("serverTime", v));
                }
                if (!health.containsKey("version")) {
                    health.put("version", "UP (version non parsée)");
                }
            } else {
                health.put("status", "UP");
                health.put("version", "UP (serverinfo HTTP " + response.statusCode() + ")");
            }
        } catch (Exception ex) {
            log.error("Fallback HTTP serverinfo also failed: {}", ex.getMessage());
            health.put("status", "DOWN");
            health.put("error", ex.getMessage());
        }
    }

    private java.util.Optional<String> extractJsonStringField(String json, String field, int searchFrom) {
        String pattern = "\"" + field + "\"";
        int idx = json.indexOf(pattern, searchFrom);
        if (idx < 0) return java.util.Optional.empty();
        int colonIdx = json.indexOf(':', idx + pattern.length());
        if (colonIdx < 0) return java.util.Optional.empty();
        int startQuote = json.indexOf('"', colonIdx + 1);
        if (startQuote < 0) return java.util.Optional.empty();
        int endQuote = json.indexOf('"', startQuote + 1);
        if (endQuote < 0) return java.util.Optional.empty();
        return java.util.Optional.of(json.substring(startQuote + 1, endQuote));
    }

    // Helper method to record history
    private void recordSyncHistory(String realmName, String type, String status, int count, LocalDateTime start,
            String errorMessage) {
        try {
            SyncHistoryEntity history = new SyncHistoryEntity();
            history.setRealmName(realmName);
            history.setSyncType(type);
            history.setStatus(status);
            history.setItemsProcessed(count);
            history.setSyncDate(LocalDateTime.now());
            history.setDurationMs(ChronoUnit.MILLIS.between(start, LocalDateTime.now()));
            history.setErrorMessage(errorMessage);

            // Persist the history entity
            syncHistoryRepository.persist(history);
        } catch (Exception e) {
            log.error("Failed to record sync history", e);
        }
    }
}
