package com.alttd.altitudeweb.services.chat.event_publisher;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

@RequiredArgsConstructor
@Service
@Slf4j
public class EventPublisher {

    private final Map<UUID, EventUser> eventUserMap = new ConcurrentHashMap<>();
    private final Map<UUID, List<SseEmitter>> emitterMap = new ConcurrentHashMap<>();

    public SseEmitter subscribe(EventUser eventUser, String initialData) {
        SseEmitter emitter = new SseEmitter(0L); // no built-in timeout; heartbeat governs liveness

        emitterMap.computeIfAbsent(eventUser.uuid(), k -> new CopyOnWriteArrayList<>()).add(emitter);
        eventUserMap.put(eventUser.uuid(), eventUser);
        log.info("User {} subscribed, active emitters: {}", eventUser.uuid(), countActive());

        Runnable cleanup = () -> {
            List<SseEmitter> userEmitters = emitterMap.get(eventUser.uuid());
            if (userEmitters != null) {
                userEmitters.remove(emitter);
                if (userEmitters.isEmpty()) {
                    emitterMap.remove(eventUser.uuid());
                    eventUserMap.remove(eventUser.uuid());
                }
            }
            log.info("Cleaned up emitter for {}, active emitters: {}", eventUser.uuid(), countActive());
        };

        // covers: clean disconnect (tab closed normally), server-side timeout, write failure
        emitter.onCompletion(cleanup);
        emitter.onTimeout(cleanup);
        emitter.onError(e -> cleanup.run());

        try {
            emitter.send(SseEmitter.event().name("connected").data(initialData));
        } catch (IOException e) {
            cleanup.run();
        }

        return emitter;
    }

    // runs every 20s: keeps idle timeouts from firing,
    // AND prunes emitters whose sockets are actually dead
    @Scheduled(fixedRate = 20_000)
    public void heartbeat() {
        emitterMap.values().forEach(list -> {
            List<SseEmitter> dead = new ArrayList<>();
            for (SseEmitter emitter : list) {
                try {
                    emitter.send(SseEmitter.event().comment("heartbeat"));
                } catch (IOException e) {
                    dead.add(emitter);
                }
            }
            list.removeAll(dead);
        });
        emitterMap.entrySet().removeIf(e -> e.getValue().isEmpty());
    }

    public void sendToUser(UUID uuid, String eventName, String json) {
        List<SseEmitter> userEmitters = emitterMap.get(uuid);
        if (userEmitters == null) {
            return;
        }

        List<SseEmitter> dead = new ArrayList<>();
        for (SseEmitter emitter : userEmitters) {
            try {
                emitter.send(SseEmitter.event().name(eventName).data(json));
            } catch (IOException e) {
                dead.add(emitter);
            }
        }
        userEmitters.removeAll(dead);
    }

    private long countActive() {
        return emitterMap.values().stream().mapToLong(List::size).sum();
    }

    public void sendToUsers(String eventName, MessageForUser messagesForUser) {
        eventUserMap.values().forEach(emitter -> {
            String json = messagesForUser.get(emitter);
            sendToUser(emitter.uuid(), eventName, json);
        });
    }
}
