package dev.lions.unionflow.server.service;

import dev.lions.unionflow.server.api.dto.backup.request.CreateBackupRequest;
import dev.lions.unionflow.server.api.dto.backup.request.RestoreBackupRequest;
import dev.lions.unionflow.server.api.dto.backup.request.UpdateBackupConfigRequest;
import dev.lions.unionflow.server.api.dto.backup.response.BackupConfigResponse;
import dev.lions.unionflow.server.api.dto.backup.response.BackupResponse;
import dev.lions.unionflow.server.entity.BackupConfig;
import dev.lions.unionflow.server.entity.BackupRecord;
import dev.lions.unionflow.server.repository.BackupConfigRepository;
import dev.lions.unionflow.server.repository.BackupRecordRepository;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.microprofile.config.inject.ConfigProperty;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * Service de gestion des sauvegardes système.
 * Persiste les métadonnées en base et exécute pg_dump pour les sauvegardes DB.
 */
@Slf4j
@ApplicationScoped
public class BackupService {

    private static final Pattern JDBC_PATTERN =
            Pattern.compile("jdbc:postgresql://([^:/]+):(\\d+)/([^?]+)");

    @Inject
    SecurityIdentity securityIdentity;

    @Inject
    BackupRecordRepository backupRecordRepository;

    @Inject
    BackupConfigRepository backupConfigRepository;

    @ConfigProperty(name = "unionflow.backup.directory", defaultValue = "/tmp/unionflow-backups")
    String backupDirectory;

    @ConfigProperty(name = "quarkus.datasource.jdbc.url")
    String jdbcUrl;

    @ConfigProperty(name = "quarkus.datasource.username")
    String dbUsername;

    @ConfigProperty(name = "quarkus.datasource.password")
    String dbPassword;

    // ==================== API PUBLIQUE ====================

    /**
     * Lister toutes les sauvegardes disponibles.
     */
    public List<BackupResponse> getAllBackups() {
        log.debug("Récupération de toutes les sauvegardes");
        return backupRecordRepository.findAllOrderedByDate()
                .stream()
                .map(this::mapToResponse)
                .collect(Collectors.toList());
    }

    /**
     * Récupérer une sauvegarde par ID.
     */
    public BackupResponse getBackupById(UUID id) {
        log.debug("Récupération de la sauvegarde: {}", id);
        return backupRecordRepository.findByIdOptional(id)
                .map(this::mapToResponse)
                .orElseThrow(() -> new RuntimeException("Sauvegarde non trouvée: " + id));
    }

    /**
     * Créer une nouvelle sauvegarde et exécuter pg_dump si la DB est incluse.
     */
    @Transactional
    public BackupResponse createBackup(CreateBackupRequest request) {
        log.info("Création d'une nouvelle sauvegarde: {}", request.getName());

        String createdBy = securityIdentity.getPrincipal() != null
                ? securityIdentity.getPrincipal().getName()
                : "system";

        boolean includesDb = request.getIncludeDatabase() == null || request.getIncludeDatabase();
        boolean includesFiles = Boolean.TRUE.equals(request.getIncludeFiles());
        boolean includesConf = request.getIncludeConfiguration() == null || request.getIncludeConfiguration();

        String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm"));
        String type = request.getType() != null ? request.getType() : "MANUAL";
        String filePath = backupDirectory + "/" + type.toLowerCase() + "-" + timestamp + ".dump";

        BackupRecord record = BackupRecord.builder()
                .name(request.getName())
                .description(request.getDescription())
                .type(type)
                .status("IN_PROGRESS")
                .createdBy(createdBy)
                .includesDatabase(includesDb)
                .includesFiles(includesFiles)
                .includesConfiguration(includesConf)
                .filePath(filePath)
                .build();

        backupRecordRepository.persist(record);

        // Execute backup synchronously (admin-triggered operation)
        long sizeBytes = 0L;
        String errorMessage = null;
        boolean success = true;

        try {
            if (includesDb) {
                Files.createDirectories(Paths.get(backupDirectory));
                success = executePgDump(filePath);
                if (success) {
                    File backupFile = new File(filePath);
                    sizeBytes = backupFile.exists() ? backupFile.length() : 0L;
                } else {
                    errorMessage = "pg_dump failed — check server logs";
                }
            }
        } catch (IOException e) {
            log.error("Erreur lors de la création du répertoire de sauvegarde", e);
            success = false;
            errorMessage = "Cannot create backup directory: " + e.getMessage();
        }

        record.setStatus(success ? "COMPLETED" : "FAILED");
        record.setSizeBytes(sizeBytes);
        record.setCompletedAt(LocalDateTime.now());
        record.setErrorMessage(errorMessage);

        log.info("Sauvegarde {} avec statut {}: {}", record.getId(), record.getStatus(), filePath);
        return mapToResponse(record);
    }

    /**
     * Restaurer une sauvegarde (crée un point de restauration au préalable si demandé).
     */
    @Transactional
    public void restoreBackup(RestoreBackupRequest request) {
        log.info("Restauration de la sauvegarde: {}", request.getBackupId());

        BackupRecord backup = backupRecordRepository.findByIdOptional(request.getBackupId())
                .orElseThrow(() -> new RuntimeException("Sauvegarde non trouvée: " + request.getBackupId()));

        if (!"COMPLETED".equals(backup.getStatus())) {
            throw new RuntimeException("La sauvegarde doit être complétée pour être restaurée");
        }

        if (Boolean.TRUE.equals(request.getCreateRestorePoint())) {
            log.info("Création d'un point de restauration avant la restauration");
            createBackup(CreateBackupRequest.builder()
                    .name("Point de restauration")
                    .description("Avant restauration de: " + backup.getName())
                    .type("RESTORE_POINT")
                    .includeDatabase(true)
                    .includeFiles(false)
                    .includeConfiguration(true)
                    .build());
        }

        // pg_restore requires the application to be stopped — log the command instead
        log.warn("Restauration initiée pour le fichier: {}", backup.getFilePath());
        log.warn("Exécutez manuellement : pg_restore -h <host> -U <user> -d <dbname> -Fc {}", backup.getFilePath());
        log.info("- Database: {}, Files: {}, Configuration: {}",
                request.getRestoreDatabase(), request.getRestoreFiles(), request.getRestoreConfiguration());
    }

    /**
     * Supprimer une sauvegarde (fichier physique + soft-delete du record).
     */
    @Transactional
    public void deleteBackup(UUID id) {
        log.info("Suppression de la sauvegarde: {}", id);

        BackupRecord backup = backupRecordRepository.findByIdOptional(id)
                .orElseThrow(() -> new RuntimeException("Sauvegarde non trouvée: " + id));

        // Supprimer le fichier physique si présent
        if (backup.getFilePath() != null) {
            File file = new File(backup.getFilePath());
            if (file.exists() && file.isFile()) {
                boolean deleted = file.delete();
                if (deleted) {
                    log.info("Fichier physique supprimé: {}", backup.getFilePath());
                } else {
                    log.warn("Impossible de supprimer le fichier: {}", backup.getFilePath());
                }
            }
        }

        // Soft delete via BaseEntity.actif
        backup.setActif(false);
        log.info("Sauvegarde {} supprimée (soft delete)", id);
    }

    /**
     * Récupérer la configuration des sauvegardes automatiques.
     */
    @Transactional
    public BackupConfigResponse getBackupConfig() {
        log.debug("Récupération de la configuration des sauvegardes");

        BackupConfig config = backupConfigRepository.getConfig()
                .orElseGet(this::createDefaultBackupConfig);

        return buildConfigResponse(config);
    }

    /**
     * Mettre à jour la configuration des sauvegardes automatiques.
     */
    @Transactional
    public BackupConfigResponse updateBackupConfig(UpdateBackupConfigRequest request) {
        log.info("Mise à jour de la configuration des sauvegardes");

        BackupConfig config = backupConfigRepository.getConfig()
                .orElseGet(this::createDefaultBackupConfig);

        if (request.getAutoBackupEnabled() != null) config.setAutoBackupEnabled(request.getAutoBackupEnabled());
        if (request.getFrequency() != null)         config.setFrequency(request.getFrequency());
        if (request.getRetentionDays() != null)     config.setRetentionDays(request.getRetentionDays());
        if (request.getBackupTime() != null)        config.setBackupTime(request.getBackupTime());
        if (request.getIncludeDatabase() != null)   config.setIncludeDatabase(request.getIncludeDatabase());
        if (request.getIncludeFiles() != null)      config.setIncludeFiles(request.getIncludeFiles());
        if (request.getIncludeConfiguration() != null) config.setIncludeConfiguration(request.getIncludeConfiguration());

        return buildConfigResponse(config);
    }

    /**
     * Créer un point de restauration avant une opération critique.
     */
    @Transactional
    public BackupResponse createRestorePoint() {
        log.info("Création d'un point de restauration");
        return createBackup(CreateBackupRequest.builder()
                .name("Point de restauration")
                .description("Point de restauration créé le " + LocalDateTime.now())
                .type("RESTORE_POINT")
                .includeDatabase(true)
                .includeFiles(false)
                .includeConfiguration(true)
                .build());
    }

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

    /**
     * Exécute pg_dump vers le fichier spécifié.
     * @return true si pg_dump s'est terminé avec le code 0
     */
    private boolean executePgDump(String filePath) {
        Matcher m = JDBC_PATTERN.matcher(jdbcUrl);
        if (!m.find()) {
            log.error("Impossible de parser l'URL JDBC: {}", jdbcUrl);
            return false;
        }
        String dbHost = m.group(1);
        String dbPort = m.group(2);
        String dbName = m.group(3);

        try {
            ProcessBuilder pb = new ProcessBuilder(
                    "pg_dump",
                    "-h", dbHost,
                    "-p", dbPort,
                    "-U", dbUsername,
                    "-Fc",           // custom format (compressed)
                    "-f", filePath,
                    dbName
            );
            pb.environment().put("PGPASSWORD", dbPassword);
            pb.redirectErrorStream(true);

            log.info("Lancement de pg_dump: {} → {}", dbName, filePath);
            Process process = pb.start();
            String output = new String(process.getInputStream().readAllBytes());
            int exitCode = process.waitFor();

            if (exitCode != 0) {
                log.error("pg_dump a échoué (code {}): {}", exitCode, output);
                return false;
            }
            log.info("pg_dump terminé avec succès pour {}", dbName);
            return true;
        } catch (IOException e) {
            log.error("pg_dump introuvable ou erreur d'exécution — vérifiez que pg_dump est dans le PATH", e);
            return false;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.error("pg_dump interrompu", e);
            return false;
        }
    }

    private BackupConfig createDefaultBackupConfig() {
        BackupConfig config = BackupConfig.builder()
                .autoBackupEnabled(true)
                .frequency("DAILY")
                .retentionDays(30)
                .backupTime("02:00")
                .includeDatabase(true)
                .includeFiles(false)
                .includeConfiguration(true)
                .backupDirectory(backupDirectory)
                .build();
        backupConfigRepository.persist(config);
        return config;
    }

    private BackupConfigResponse buildConfigResponse(BackupConfig config) {
        long totalBackups = backupRecordRepository.count("status = ?1 AND actif = ?2", "COMPLETED", true);
        long totalSizeBytes = backupRecordRepository
                .find("status = ?1 AND actif = ?2 AND sizeBytes IS NOT NULL", "COMPLETED", true)
                .list()
                .stream()
                .mapToLong(r -> r.getSizeBytes() != null ? r.getSizeBytes() : 0L)
                .sum();

        LocalDateTime lastBackup = backupRecordRepository
                .find("status = ?1 AND actif = ?2 ORDER BY completedAt DESC", "COMPLETED", true)
                .firstResultOptional()
                .map(BackupRecord::getCompletedAt)
                .orElse(null);

        return BackupConfigResponse.builder()
                .autoBackupEnabled(config.getAutoBackupEnabled())
                .frequency(config.getFrequency())
                .retention(config.getRetentionDays() + " jours")
                .retentionDays(config.getRetentionDays())
                .backupTime(config.getBackupTime())
                .includeDatabase(config.getIncludeDatabase())
                .includeFiles(config.getIncludeFiles())
                .includeConfiguration(config.getIncludeConfiguration())
                .lastBackup(lastBackup)
                .nextScheduledBackup(calculateNextBackup(config.getFrequency(), config.getBackupTime()))
                .totalBackups((int) totalBackups)
                .totalSizeBytes(totalSizeBytes)
                .totalSizeFormatted(formatBytes(totalSizeBytes))
                .build();
    }

    private BackupResponse mapToResponse(BackupRecord record) {
        return BackupResponse.builder()
                .id(record.getId())
                .name(record.getName())
                .description(record.getDescription())
                .type(record.getType())
                .sizeBytes(record.getSizeBytes() != null ? record.getSizeBytes() : 0L)
                .sizeFormatted(formatBytes(record.getSizeBytes() != null ? record.getSizeBytes() : 0L))
                .status(record.getStatus())
                .createdAt(record.getDateCreation())
                .completedAt(record.getCompletedAt())
                .createdBy(record.getCreatedBy())
                .includesDatabase(record.getIncludesDatabase())
                .includesFiles(record.getIncludesFiles())
                .includesConfiguration(record.getIncludesConfiguration())
                .filePath(record.getFilePath())
                .build();
    }

    private LocalDateTime calculateNextBackup(String frequency, String backupTime) {
        LocalTime time = backupTime != null ? LocalTime.parse(backupTime) : LocalTime.of(2, 0);
        LocalDateTime next = LocalDateTime.now().with(time);

        if (frequency == null) frequency = "DAILY";
        switch (frequency) {
            case "HOURLY":
                return LocalDateTime.now().plusHours(1);
            case "WEEKLY":
                if (next.isBefore(LocalDateTime.now())) next = next.plusWeeks(1);
                return next;
            default: // DAILY
                if (next.isBefore(LocalDateTime.now())) next = next.plusDays(1);
                return next;
        }
    }

    private String formatBytes(long bytes) {
        if (bytes <= 0) return "0 B";
        if (bytes < 1024) return bytes + " B";
        int exp = (int) (Math.log(bytes) / Math.log(1024));
        char pre = "KMGTPE".charAt(exp - 1);
        return String.format("%.1f %sB", bytes / Math.pow(1024, exp), pre);
    }
}
