package de.assecutor.votianlt.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import de.assecutor.votianlt.messaging.delivery.MessageDeliveryService;
import de.assecutor.votianlt.messaging.plugin.PluginManager;
import de.assecutor.votianlt.messaging.plugin.SendOptions;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Service for managing client connections via Ping/Pong mechanism.
 * Tracks connected clients and periodically checks their connectivity.
 */
@Service
@Slf4j
public class ClientConnectionService {

    /**
     * Represents the connection state of a client.
     */
    public record ClientState(
            String clientId,
            String userId,
            boolean connected,
            Instant lastPingSent,
            Instant lastPongReceived,
            Instant connectedAt
    ) {
        public ClientState withPingSent(Instant pingSent) {
            return new ClientState(clientId, userId, connected, pingSent, lastPongReceived, connectedAt);
        }

        public ClientState withPongReceived(Instant pongReceived) {
            return new ClientState(clientId, userId, true, lastPingSent, pongReceived, connectedAt);
        }

        public ClientState withConnected(boolean isConnected) {
            return new ClientState(clientId, userId, isConnected, lastPingSent, lastPongReceived, connectedAt);
        }
    }

    private final Map<String, ClientState> connectedClients = new ConcurrentHashMap<>();
    private final PluginManager pluginManager;
    private final ObjectMapper objectMapper;
    private final MessageDeliveryService messageDeliveryService;

    @Value("${app.client.ping.interval-seconds:15}")
    private int pingIntervalSeconds;

    @Value("${app.client.ping.timeout-seconds:5}")
    private int pingTimeoutSeconds;

    public ClientConnectionService(PluginManager pluginManager, ObjectMapper objectMapper,
                                   @Lazy MessageDeliveryService messageDeliveryService) {
        this.pluginManager = pluginManager;
        this.objectMapper = objectMapper;
        this.messageDeliveryService = messageDeliveryService;
    }

    /**
     * Registers a client as connected after successful login.
     *
     * @param clientId The unique client identifier
     * @param userId   The user ID associated with this client
     */
    public void registerClient(String clientId, String userId) {
        if (clientId == null || clientId.isBlank()) {
            log.warn("Cannot register client with null or blank clientId");
            return;
        }

        ClientState previousState = connectedClients.get(clientId);
        boolean wasDisconnected = previousState != null && !previousState.connected();

        Instant now = Instant.now();
        ClientState state = new ClientState(clientId, userId, true, null, now, now);
        connectedClients.put(clientId, state);
        log.info("[ClientConnectionService] Client registered: clientId={}, userId={}, totalClients={}",
                clientId, userId, connectedClients.size());

        // If client was previously disconnected, retry pending messages
        if (wasDisconnected) {
            log.info("Client {} re-registered after disconnect - triggering pending message retry", clientId);
            messageDeliveryService.retryPendingDeliveriesForClient(clientId);
        }
    }

    /**
     * Unregisters a client (e.g., on explicit logout).
     *
     * @param clientId The client identifier to unregister
     */
    public void unregisterClient(String clientId) {
        ClientState removed = connectedClients.remove(clientId);
        if (removed != null) {
            log.info("Client unregistered: clientId={}, userId={}", clientId, removed.userId());
        }
    }

    /**
     * Handles a pong response from a client.
     * Searches by both clientId and userId since pong is sent to /server/{userId}/pong.
     *
     * @param id The client or user identifier that sent the pong
     */
    public void handlePong(String id) {
        if (id == null || id.isBlank()) {
            log.warn("Received pong from null or blank id");
            return;
        }

        // First try direct lookup by clientId
        String clientId = id;
        ClientState state = connectedClients.get(id);

        // If not found, search by userId
        if (state == null) {
            for (Map.Entry<String, ClientState> entry : connectedClients.entrySet()) {
                if (id.equals(entry.getValue().userId())) {
                    clientId = entry.getKey();
                    state = entry.getValue();
                    break;
                }
            }
        }

        if (state != null) {
            boolean wasDisconnected = !state.connected();
            ClientState updatedState = state.withPongReceived(Instant.now());
            connectedClients.put(clientId, updatedState);

            // If client was disconnected and is now reconnected, retry pending messages
            if (wasDisconnected) {
                log.info("Client {} reconnected via pong - triggering pending message retry", clientId);
                messageDeliveryService.retryPendingDeliveriesForClient(clientId);
            }
        } else {
            log.warn("Received pong from unknown id: {}", id);
        }
    }

    /**
     * Checks if a client is currently connected.
     * Searches by both clientId and userId.
     *
     * @param id The client or user identifier
     * @return true if the client is connected
     */
    public boolean isClientConnected(String id) {
        if (id == null || id.isBlank()) {
            return false;
        }
        // First try direct lookup by clientId
        ClientState state = connectedClients.get(id);
        if (state != null && state.connected()) {
            return true;
        }
        // Then search by userId
        return connectedClients.values().stream()
                .anyMatch(s -> s.connected() && id.equals(s.userId()));
    }

    /**
     * Gets all connected client IDs.
     *
     * @return Set of connected client IDs
     */
    public Set<String> getConnectedClientIds() {
        return connectedClients.entrySet().stream()
                .filter(e -> e.getValue().connected())
                .map(Map.Entry::getKey)
                .collect(java.util.stream.Collectors.toSet());
    }

    /**
     * Gets the connection state for a specific client.
     *
     * @param clientId The client identifier
     * @return ClientState or null if not found
     */
    public ClientState getClientState(String clientId) {
        return connectedClients.get(clientId);
    }

    /**
     * Scheduled task to send pings to all connected clients.
     * Runs based on the configured interval (app.client.ping.interval-seconds).
     */
    @Scheduled(fixedRateString = "${app.client.ping.interval-seconds:15}000")
    public void sendPingsToAllClients() {
        log.info("[ClientConnectionService] Ping cycle started - pluginConnected={}, connectedClients={}",
                pluginManager.isConnected(), getConnectedClientCount());

        if (!pluginManager.isConnected()) {
            log.info("[ClientConnectionService] Plugin not connected, skipping ping cycle");
            return;
        }

        int connectedCount = getConnectedClientCount();
        if (connectedCount == 0) {
            log.info("[ClientConnectionService] No connected clients, skipping ping cycle");
            return;
        }

        log.info("[ClientConnectionService] Starting ping cycle for {} clients", connectedCount);
        Instant now = Instant.now();

        for (Map.Entry<String, ClientState> entry : connectedClients.entrySet()) {
            String clientId = entry.getKey();
            ClientState state = entry.getValue();

            // Check if previous ping timed out
            if (state.lastPingSent() != null && state.connected()) {
                Instant expectedPongBy = state.lastPingSent().plusSeconds(pingTimeoutSeconds);
                boolean pongReceivedAfterPing = state.lastPongReceived() != null
                        && state.lastPongReceived().isAfter(state.lastPingSent());

                if (now.isAfter(expectedPongBy) && !pongReceivedAfterPing) {
                    // Client did not respond in time - mark as disconnected
                    ClientState disconnectedState = state.withConnected(false);
                    connectedClients.put(clientId, disconnectedState);
                    log.warn("Client timed out, marking as disconnected: clientId={}, userId={}",
                            clientId, state.userId());
                    continue;
                }
            }

            // Send ping to connected clients (use userId for topic)
            if (state.connected() && state.userId() != null) {
                sendPing(state.userId());
                ClientState updatedState = state.withPingSent(now);
                connectedClients.put(clientId, updatedState);
            }
        }
    }

    /**
     * Sends a ping message to a specific user.
     *
     * @param userId The target user ID (MongoDB ObjectId)
     */
    private void sendPing(String userId) {
        try {
            Map<String, Object> pingPayload = Map.of(
                    "type", "ping",
                    "timestamp", Instant.now().toEpochMilli()
            );

            String json = objectMapper.writeValueAsString(pingPayload);
            byte[] payload = json.getBytes(StandardCharsets.UTF_8);

            SendOptions options = SendOptions.builder()
                    .qos(1)
                    .retained(false)
                    .build();

            pluginManager.sendToClient(userId, "ping", payload, options);

        } catch (Exception e) {
            log.error("Error sending ping to user {}: {}", userId, e.getMessage());
        }
    }

    /**
     * Gets the number of currently connected clients.
     *
     * @return Number of connected clients
     */
    public int getConnectedClientCount() {
        return (int) connectedClients.values().stream()
                .filter(ClientState::connected)
                .count();
    }

    /**
     * Gets the total number of registered clients (connected and disconnected).
     *
     * @return Total number of registered clients
     */
    public int getTotalClientCount() {
        return connectedClients.size();
    }
}
