package de.assecutor.votianlt.messaging.delivery;

import com.fasterxml.jackson.databind.ObjectMapper;
import de.assecutor.votianlt.messaging.model.*;
import de.assecutor.votianlt.messaging.plugin.PluginManager;
import de.assecutor.votianlt.messaging.plugin.SendOptions;
import de.assecutor.votianlt.repository.PendingDeliveryRepository;
import de.assecutor.votianlt.service.ClientConnectionService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * Implementation of MessageDeliveryService with reliable delivery guarantees.
 */
@Service
@Slf4j
public class MessageDeliveryServiceImpl implements MessageDeliveryService {

    @Value("${app.messaging.delivery.ack-retry-interval-seconds:5}")
    private int ackRetryIntervalSeconds;

    @Value("${app.messaging.delivery.ack-max-retries:4}")
    private int ackMaxRetries;

    private final PluginManager pluginManager;
    private final PendingDeliveryRepository pendingDeliveryRepository;
    private final AcknowledgmentHandler acknowledgmentHandler;
    private final DeliveryConfig config;
    private final ObjectMapper objectMapper;
    private final ClientConnectionService clientConnectionService;

    private ScheduledExecutorService retryScheduler;

    public MessageDeliveryServiceImpl(
            PluginManager pluginManager,
            PendingDeliveryRepository pendingDeliveryRepository,
            AcknowledgmentHandler acknowledgmentHandler,
            DeliveryConfig config,
            ObjectMapper objectMapper,
            ClientConnectionService clientConnectionService) {
        this.pluginManager = pluginManager;
        this.pendingDeliveryRepository = pendingDeliveryRepository;
        this.acknowledgmentHandler = acknowledgmentHandler;
        this.config = config;
        this.objectMapper = objectMapper;
        this.clientConnectionService = clientConnectionService;
    }

    @PostConstruct
    public void startRetryScheduler() {
        retryScheduler = Executors.newSingleThreadScheduledExecutor(r -> {
            Thread t = new Thread(r, "message-retry-scheduler");
            t.setDaemon(true);
            return t;
        });
        retryScheduler.scheduleAtFixedRate(
                this::retryPendingDeliveries,
                ackRetryIntervalSeconds,
                ackRetryIntervalSeconds,
                TimeUnit.SECONDS
        );
        log.info("[MessageDelivery] Started retry scheduler (interval: {}s, max retries: {})",
                ackRetryIntervalSeconds, ackMaxRetries);
    }

    @PreDestroy
    public void stopRetryScheduler() {
        if (retryScheduler != null) {
            retryScheduler.shutdown();
            try {
                if (!retryScheduler.awaitTermination(5, TimeUnit.SECONDS)) {
                    retryScheduler.shutdownNow();
                }
            } catch (InterruptedException e) {
                retryScheduler.shutdownNow();
                Thread.currentThread().interrupt();
            }
            log.info("[MessageDelivery] Stopped retry scheduler");
        }
    }

    @Override
    public CompletableFuture<DeliveryReceipt> sendToClient(String clientId, String messageType, Object payload, DeliveryOptions options) {
        try {
            String destination = clientId + "/" + messageType;
            final LocalDateTime expiresAt = options.calculateExpiryTime();
            MessageEnvelope envelope = new MessageEnvelope(destination, payload, options.isRequiresAck(), expiresAt);
            final String messageId = envelope.getMessageId();

            String json = objectMapper.writeValueAsString(envelope);
            byte[] envelopeData = json.getBytes(StandardCharsets.UTF_8);

            if (options.isRequiresAck()) {
                PendingDelivery pending = new PendingDelivery(
                    messageId,
                    destination,
                    envelopeData,
                    options.getMaxRetries(),
                    expiresAt
                );
                pendingDeliveryRepository.save(pending);
            }

            SendOptions sendOptions = SendOptions.builder()
                    .qos(options.getQos())
                    .retained(options.isRetained())
                    .build();

            final boolean requiresAck = options.isRequiresAck();
            final Duration ackTimeout = options.getAckTimeout();

            log.info("[MessageDelivery] Sending message {} to client {} (type: {})", messageId, clientId, messageType);

            return pluginManager.sendToClient(clientId, messageType, envelopeData, sendOptions)
                    .thenApply(v -> {
                        if (requiresAck) {
                            updatePendingDeliveryAfterSend(messageId, ackTimeout);
                        }
                        return DeliveryReceipt.submitted(messageId, destination, expiresAt);
                    })
                    .exceptionally(ex -> {
                        log.error("[MessageDelivery] Failed to send message {}: {}", messageId, ex.getMessage());
                        if (requiresAck) {
                            markPendingDeliveryFailed(messageId, ex.getMessage());
                        }
                        return DeliveryReceipt.failed(messageId, destination);
                    });

        } catch (Exception e) {
            log.error("[MessageDelivery] Error creating message for client {}: {}", clientId, e.getMessage());
            return CompletableFuture.completedFuture(DeliveryReceipt.failed("error", clientId + "/" + messageType));
        }
    }

    @Override
    @Deprecated
    public CompletableFuture<DeliveryReceipt> sendMessage(String topic, Object payload, DeliveryOptions options) {
        String[] parts = topic.split("/");
        if (parts.length >= 4 && parts[1].equals("client")) {
            String clientId = parts[2];
            String messageType = parts[3];
            return sendToClient(clientId, messageType, payload, options);
        }
        log.warn("[MessageDelivery] Using deprecated sendMessage with topic: {}", topic);
        return CompletableFuture.completedFuture(DeliveryReceipt.failed("error", topic));
    }

    @Override
    public void handleIncomingMessage(MessageEnvelope envelope) {
        try {
            log.info("[MessageDelivery] Received message {} on topic {}",
                    envelope.getMessageId(), envelope.getTopic());

            if (envelope.isRequiresAck()) {
                sendAcknowledgment(envelope);
            }

            acknowledgmentHandler.routeIncomingMessage(envelope);

        } catch (Exception e) {
            log.error("[MessageDelivery] Error handling incoming message {}: {}",
                    envelope.getMessageId(), e.getMessage());
        }
    }

    @Override
    public void handleAcknowledgment(AcknowledgmentMessage ack) {
        try {
            log.info("[MessageDelivery] Received ACK for message {} (status: {})",
                    ack.getMessageId(), ack.getStatus());

            Optional<PendingDelivery> pendingOpt = pendingDeliveryRepository.findByMessageId(ack.getMessageId());

            if (pendingOpt.isEmpty()) {
                return;
            }

            PendingDelivery pending = pendingOpt.get();

            switch (ack.getStatus()) {
                case RECEIVED, PROCESSED -> {
                    pendingDeliveryRepository.delete(pending);
                }
                case FAILED -> {
                    pending.markAsFailed(ack.getErrorMessage());
                    pendingDeliveryRepository.save(pending);
                    log.warn("[MessageDelivery] Message {} failed on client: {}",
                            ack.getMessageId(), ack.getErrorMessage());
                }
            }

        } catch (Exception e) {
            log.error("[MessageDelivery] Error handling ACK for message {}: {}",
                    ack.getMessageId(), e.getMessage());
        }
    }

    @Override
    public Optional<DeliveryStatus> getDeliveryStatus(String messageId) {
        return pendingDeliveryRepository.findByMessageId(messageId)
                .map(PendingDelivery::getStatus);
    }

    @Override
    public Optional<PendingDelivery> getPendingDelivery(String messageId) {
        return pendingDeliveryRepository.findByMessageId(messageId);
    }

    @Override
    public void retryPendingDeliveries() {
        try {
            List<PendingDelivery> readyForRetry = pendingDeliveryRepository
                    .findByStatusAndNextRetryAtBefore(DeliveryStatus.SENT, LocalDateTime.now());

            if (readyForRetry.isEmpty()) {
                return;
            }

            for (PendingDelivery pending : readyForRetry) {
                retryDelivery(pending);
            }

        } catch (Exception e) {
            log.error("[MessageDelivery] Error during retry process: {}", e.getMessage());
        }
    }

    @Override
    public void retryPendingDeliveriesForClient(String clientId) {
        if (clientId == null || clientId.isBlank()) {
            return;
        }

        try {
            List<PendingDelivery> pendingDeliveries = pendingDeliveryRepository
                    .findByStatusIn(List.of(DeliveryStatus.PENDING, DeliveryStatus.SENT));

            for (PendingDelivery pending : pendingDeliveries) {
                String topic = pending.getTopic();
                if (topic != null && topic.startsWith(clientId + "/")) {
                    retryDelivery(pending);
                }
            }

        } catch (Exception e) {
            log.error("[MessageDelivery] Error retrying deliveries for client {}: {}", clientId, e.getMessage());
        }
    }

    @Override
    public void cleanupOldDeliveries() {
        try {
            LocalDateTime cutoff = LocalDateTime.now().minus(Duration.ofDays(7));
            List<PendingDelivery> oldAcknowledged = pendingDeliveryRepository
                    .findByStatusAndAcknowledgedAtBefore(DeliveryStatus.ACKNOWLEDGED, cutoff);

            if (!oldAcknowledged.isEmpty()) {
                pendingDeliveryRepository.deleteAll(oldAcknowledged);
            }

            List<PendingDelivery> expired = pendingDeliveryRepository
                    .findByStatusInAndExpiresAtBefore(
                            List.of(DeliveryStatus.PENDING, DeliveryStatus.SENT),
                            LocalDateTime.now()
                    );

            for (PendingDelivery pending : expired) {
                pending.markAsExpired();
                pendingDeliveryRepository.save(pending);
            }

        } catch (Exception e) {
            log.error("[MessageDelivery] Error during cleanup: {}", e.getMessage());
        }
    }

    private void updatePendingDeliveryAfterSend(String messageId, Duration ackTimeout) {
        try {
            Optional<PendingDelivery> pendingOpt = pendingDeliveryRepository.findByMessageId(messageId);
            if (pendingOpt.isPresent()) {
                PendingDelivery pending = pendingOpt.get();
                LocalDateTime nextRetry = LocalDateTime.now().plus(ackTimeout);
                pending.markAsSent(nextRetry);
                pendingDeliveryRepository.save(pending);
            }
        } catch (Exception e) {
            log.error("[MessageDelivery] Error updating pending delivery {}: {}", messageId, e.getMessage());
        }
    }

    private void markPendingDeliveryFailed(String messageId, String reason) {
        try {
            Optional<PendingDelivery> pendingOpt = pendingDeliveryRepository.findByMessageId(messageId);
            if (pendingOpt.isPresent()) {
                PendingDelivery pending = pendingOpt.get();
                pending.markAsFailed(reason);
                pendingDeliveryRepository.save(pending);
            }
        } catch (Exception e) {
            log.error("[MessageDelivery] Error marking delivery as failed {}: {}", messageId, e.getMessage());
        }
    }

    private void retryDelivery(PendingDelivery pending) {
        try {
            if (pending.isExpired()) {
                pending.markAsExpired();
                pendingDeliveryRepository.save(pending);
                return;
            }

            if (pending.hasReachedMaxRetries()) {
                pending.markAsFailed("Max retries reached");
                pendingDeliveryRepository.save(pending);
                return;
            }

            String topic = pending.getTopic();
            String[] parts = topic.split("/");
            if (parts.length < 2) {
                pending.markAsFailed("Invalid topic format");
                pendingDeliveryRepository.save(pending);
                return;
            }

            String clientId = parts[0];
            String messageType = parts[1];

            Duration backoffDelay = calculateBackoff(pending.getRetryCount() + 1);
            LocalDateTime nextRetry = LocalDateTime.now().plus(backoffDelay);

            if (!clientConnectionService.isClientConnected(clientId)) {
                pending.setNextRetryAt(nextRetry);
                pendingDeliveryRepository.save(pending);
                return;
            }

            pending.incrementRetryCount();

            SendOptions options = SendOptions.reliable();
            pluginManager.sendToClient(clientId, messageType, pending.getEnvelopeData(), options)
                    .thenAccept(v -> {
                        pending.markAsSent(nextRetry);
                        pendingDeliveryRepository.save(pending);
                    })
                    .exceptionally(ex -> {
                        log.error("[MessageDelivery] Retry failed for message {}: {}", pending.getMessageId(), ex.getMessage());
                        pending.markAsFailed(ex.getMessage());
                        pendingDeliveryRepository.save(pending);
                        return null;
                    });

        } catch (Exception e) {
            log.error("[MessageDelivery] Error retrying delivery {}: {}", pending.getMessageId(), e.getMessage());
        }
    }

    private void sendAcknowledgment(MessageEnvelope envelope) {
        try {
            String clientId = extractClientIdFromTopic(envelope.getTopic());
            if (clientId == null) {
                return;
            }

            AcknowledgmentMessage ack = new AcknowledgmentMessage(
                    envelope.getMessageId(),
                    AckStatus.RECEIVED,
                    "server"
            );

            String ackJson = objectMapper.writeValueAsString(ack);
            byte[] ackData = ackJson.getBytes(StandardCharsets.UTF_8);

            log.info("[MessageDelivery] Sending ACK for message {} to client {}", envelope.getMessageId(), clientId);

            pluginManager.sendAckToClient(clientId, envelope.getMessageId(), ackData, SendOptions.fireAndForget())
                    .exceptionally(ex -> {
                        log.error("[MessageDelivery] Failed to send ACK for message {}: {}", envelope.getMessageId(), ex.getMessage());
                        return null;
                    });

        } catch (Exception e) {
            log.error("[MessageDelivery] Error sending ACK for message {}: {}", envelope.getMessageId(), e.getMessage());
        }
    }

    private Duration calculateBackoff(int retryCount) {
        long delayMs = (long) (config.getRetryInitialDelay().toMillis()
                * Math.pow(config.getRetryBackoffMultiplier(), retryCount - 1));
        long maxDelayMs = config.getRetryMaxDelay().toMillis();
        return Duration.ofMillis(Math.min(delayMs, maxDelayMs));
    }

    private String extractClientIdFromTopic(String topic) {
        if (topic == null) {
            return null;
        }

        if (topic.startsWith("/server/")) {
            String[] parts = topic.split("/");
            if (parts.length > 2) {
                return parts[2];
            }
        }

        if (topic.contains("/")) {
            String[] parts = topic.split("/");
            if (parts.length >= 1) {
                return parts[0];
            }
        }

        return null;
    }
}
