package ru.kirillius.pf.sdn.service;

import jakarta.annotation.PreDestroy;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
import ru.kirillius.java.utils.events.ConcurrentEventHandler;
import ru.kirillius.java.utils.events.EventHandler;
import ru.kirillius.pf.sdn.Util.WorkerUtils;
import ru.kirillius.pf.sdn.api.Networking.Subscriptions.CacheFallbackProviderProtocol;
import ru.kirillius.pf.sdn.api.Networking.Subscriptions.Subscription;
import ru.kirillius.pf.sdn.api.Networking.Subscriptions.SubscriptionProvider;
import ru.kirillius.pf.sdn.api.Networking.Subscriptions.SubscriptionProviderProtocol;
import ru.kirillius.pf.sdn.entity.SubscriptionGroup;
import ru.kirillius.pf.sdn.repository.SubscriptionProviderConfigRepository;
import ru.kirillius.pf.sdn.repository.SubscriptionSetRepository;

import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;

@Service
@RequiredArgsConstructor
@Slf4j
public class SubscriptionService {

    private final SubscriptionProviderConfigRepository configRepository;
    private final ExecutorService executorService;
    private final RuntimeConfigService runtimeConfigService;
    private final Map<String, SubscriptionProviderProtocol> protocols = new ConcurrentHashMap<>();
    private final Map<String, SubscriptionProvider> providers = new ConcurrentHashMap<>();
    private final AtomicInteger updateCounter = new AtomicInteger(0);
    private final List<Subscription> subscriptions = new CopyOnWriteArrayList<>();
    private final ApplicationContext context;
    private final SubscriptionSetRepository subscriptionSetRepository;

    @Getter
    private final EventHandler<Subscription> updateEvent = new ConcurrentEventHandler<>();

    @Getter
    private final EventHandler<SubscriptionGroup> setUpdateEvent = new ConcurrentEventHandler<>();
    private Future<?> worker;

    public List<Subscription> getSubscriptions() {
        return Collections.unmodifiableList(subscriptions);
    }

    public void update() {
        updateCounter.incrementAndGet();
    }

    @PreDestroy
    private void destroy() {
        if (worker != null) {
            worker.cancel(true);
            worker = null;
        }
    }

    public void reloadProviders() {
        var beanFactory = context.getAutowireCapableBeanFactory();
        synchronized (providers) {
            providers.clear();
            configRepository.findAll().forEach(config -> {
                var protocol = protocols.get(config.getProtocolId());
                if (protocol == null) {
                    log.error("Provider protocol with id {} not found. Using fallback from cache.", config.getProtocolId());
                    protocol = beanFactory.createBean(CacheFallbackProviderProtocol.class);
                }
                providers.put(config.getName(), SubscriptionProvider.builder()
                        .config(config)
                        .protocol(protocol)
                        .build()
                );
            });
        }
    }

    private void reloadSubscriptions() {
        var retrieved = new ArrayList<Subscription>();
        var updated = new ArrayList<Subscription>();

        var sets = subscriptionSetRepository.findAll();

        providers.values().forEach(provider -> {
            updated.addAll(provider.update());
            retrieved.addAll(provider.getSubscriptions());
        });

        synchronized (subscriptions) {
            subscriptions.clear();
            subscriptions.addAll(retrieved);
        }

        updated.forEach(s -> {
            invokeEvent(updateEvent, s);
            sets.forEach(set -> {
                if (set.getSubscriptions().contains(s)) { //TODO suspicious call???
                    invokeEvent(setUpdateEvent, set);
                }
            });
        });
    }

    private <T> void invokeEvent(EventHandler<T> eventHandler, T data) {
        try {
            eventHandler.invoke(data);
        } catch (Exception e) {
            log.error("Failed to invoke subscription update event because of error {}:{}", e.getClass().getSimpleName(), e.getMessage());
        }
    }


    private void start() {
        reloadProviders();
        reloadSubscriptions();

        worker = executorService.submit(new WorkerUtils.LoopedWorker() {
            private Instant lastUpdate = Instant.now();

            @Override
            protected void doWork() {
                if (updateCounter.get() > 0) {
                    updateCounter.set(0);
                }
                reloadSubscriptions();
                lastUpdate = Instant.now();
                var config = runtimeConfigService.getConfig();

                WorkerUtils.waitForCondition(() -> Duration.between(lastUpdate, Instant.now())
                        .compareTo(Duration.ofMinutes(config.getSubscriptionUpdateInterval())) > 0
                        || updateCounter.get() > 0, 1000L);
            }
        });

    }

    public void registerProvider(SubscriptionProviderProtocol subscriptionProviderProtocol) {
        if (subscriptionProviderProtocol == null) {
            throw new IllegalArgumentException();
        }
        var identifier = subscriptionProviderProtocol.getIdentifier();
        if (identifier == null || identifier.isBlank()) {
            throw new IllegalArgumentException("Identifier is null or blank");
        }
        if (protocols.containsKey(identifier)) {
            return;
        }
        protocols.put(identifier, subscriptionProviderProtocol);
    }

}
