package ru.dragonestia.kubik.discovery.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import ru.dragonestia.kubik.discovery.event.ServerRegisterEvent;
import ru.dragonestia.kubik.discovery.event.ServerUnregisterEvent;
import ru.dragonestia.kubik.discovery.model.Server;
import ru.dragonestia.kubik.discovery.model.ServerIdentifierStrategy;

import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

@Log4j2
@RequiredArgsConstructor
@Service
public class ServerService {

    private final ApplicationEventPublisher eventPublisher;
    private final ServerIdentifierStrategy serverIdentifierStrategy;

    private final Map<String, Server> servers = new ConcurrentHashMap<>();
    private final Map<String, Set<Server>> groupedServers = new ConcurrentHashMap<>();
    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    @Value("${kubik.activity-timeout}")
    private int activityTimeout;

    @Scheduled(timeUnit = TimeUnit.SECONDS, fixedDelay = 1)
    void gc() {
        lock.writeLock().lock();
        try {
            var now = Instant.now();
            var timeout = new ArrayList<Server>();
            for (var server: servers.values()) {
                if (server.getLastActivity().plusSeconds(activityTimeout).isAfter(now)) continue;
                timeout.add(server);

                log.warn("Detected inactive server: {}", server);
            }
            timeout.forEach(server -> unregister(server.getId()));
        } finally {
            lock.writeLock().unlock();
        }
    }

    public Server register(String groupId, String address, int port) {
        lock.writeLock().lock();
        try {
            var server = new Server(serverIdentifierStrategy, groupId, address, port);
            var lastServer = servers.put(server.getId(), server);

            if (server.equals(lastServer)) return server;
            log.info("Registering server with groupId={} address={} port={}", groupId, address, port);

            groupedServers.computeIfAbsent(groupId, _ -> new HashSet<>()).add(server);
            eventPublisher.publishEvent(new ServerRegisterEvent(server.getId(), address, port));
            return server;
        } finally {
            lock.writeLock().unlock();
        }
    }

    public void unregister(String id) {
        lock.writeLock().lock();
        try {
            log.info("Unregistering server {}", id);

            var server = servers.remove(id);
            if (server != null) {
                var group = groupedServers.get(server.getGroupId());
                if (group != null) group.remove(server);
            }

            eventPublisher.publishEvent(new ServerUnregisterEvent(id));
        } finally {
            lock.writeLock().unlock();
        }
    }

    public Server pick(String groupId) {
        lock.readLock().lock();
        try {
            for (var server: groupedServers.get(groupId)) {
                if (!server.isCanPicked()) continue;
                return server;
            }
            return null;
        } finally {
            lock.readLock().unlock();
        }
    }

    public Optional<Server> find(String id) {
        lock.readLock().lock();
        try {
            return Optional.ofNullable(servers.get(id));
        } finally {
            lock.readLock().unlock();
        }
    }

    public Collection<Server> getAllServers() {
        lock.readLock().lock();
        try {
            return servers.values();
        } finally {
            lock.readLock().unlock();
        }
    }
}
