package ru.kirillius.pf.sdn.service;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import ru.kirillius.java.utils.events.EventListener;
import ru.kirillius.pf.sdn.api.Networking.Subscriptions.Subscription;
import ru.kirillius.pf.sdn.api.pipeline.Action;
import ru.kirillius.pf.sdn.api.pipeline.PipelineFunction;
import ru.kirillius.pf.sdn.api.pipeline.ProcessingPipeline;
import ru.kirillius.pf.sdn.api.pipeline.TriggerType;
import ru.kirillius.pf.sdn.entity.ActionConfig;
import ru.kirillius.pf.sdn.entity.PipelineConfig;
import ru.kirillius.pf.sdn.repository.PipelineConfigRepository;

import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.regex.Pattern;

@Slf4j
@Service
@RequiredArgsConstructor
public class PipelineService {

    private final PipelineConfigRepository configRepository;
    private final PipelineExecutorService pipelineExecutorService;
    private final SubscriptionService subscriptionService;
    private final ExecutorService executorService;

    private final Map<PipelineConfig, ProcessingPipeline> pipelines = new ConcurrentHashMap<>();
    private final Map<String, Class<? extends PipelineFunction>> functions = new ConcurrentHashMap<>();
    private EventListener<ProcessingPipeline> executedListener;
    private EventListener<Subscription> subscriptionUpdateListener;

    public void registerFunction(Class<? extends PipelineFunction> functionClass, String id) {
        if (functions.containsKey(id)) {
            throw new IllegalStateException("Function with id '" + id + "' already exists");
        }
        functions.put(id, functionClass);
    }

    public void trigger(long id) {
        pipelines.keySet().stream().filter(config -> config.getId().equals(id)).findFirst().ifPresent(this::trigger);
    }

    public void trigger(PipelineConfig pipelineConfig) {
        if (pipelines.containsKey(pipelineConfig)) {
            trigger(pipelines.get(pipelineConfig));
        }
    }

    public void trigger(ProcessingPipeline pipeline) {
        var matchedConditions = pipeline.getConfig().getStartConditions().stream().filter(c -> c.getTrigger() == TriggerType.Manual).toList();
        if (matchedConditions.isEmpty()) {
            log.error("Unable to manual start pipeline {} because it has no manual trigger", pipeline.getConfig().getName());
            return;
        }
        matchedConditions.forEach(c -> {
            if (c.isDontStartIfRunning() && pipeline.isRunning()) {
                return;
            }
            pipelineExecutorService.triggerExecute(pipeline);
        });

    }

    public void update(PipelineConfig pipelineConfig) {
        if (configRepository.existsByGuid(pipelineConfig.getGuid())) {
            load(pipelineConfig);
        } else {
            pipelineExecutorService.cancel(pipelines.get(pipelineConfig));
            pipelines.remove(pipelineConfig);
        }
    }

    private void load(PipelineConfig pipelineConfig) {
        if (pipelines.containsKey(pipelineConfig)) {
            pipelineExecutorService.cancel(pipelines.get(pipelineConfig));
        }
        pipelines.put(pipelineConfig, new ProcessingPipeline(pipelineConfig, pipelineConfig.getActions().stream().map(this::buildAction).toList()));
    }

    private Action buildAction(ActionConfig config) {
        return new Action(functions.getOrDefault(config.getFunctionId(), DummyFunction.class), config.getProperties());
    }

    private void checkForIntervalExecution(ProcessingPipeline pipeline) {

        var matchedConditions = pipeline.getConfig().getStartConditions().stream().filter(c -> c.getTrigger() == TriggerType.Interval).toList();

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

        matchedConditions.forEach(c -> {
            var properties = c.getProperties();
            var timer = properties.getOrDefault("interval", null);
            if (timer == null) {
                return;
            }
            var delay = Long.parseLong(timer);
            if (delay <= 0) {
                return;
            }

            executorService.submit(() -> {
                try {
                    Thread.sleep(delay * 60000L);
                } catch (InterruptedException e) {
                    return;
                }
                if (c.isDontStartIfRunning() && pipeline.isRunning()) {
                    return;
                }
                pipelineExecutorService.triggerExecute(pipeline);
            });
        });

    }

    @PostConstruct
    private void initialize() {
        subscriptionUpdateListener = subscriptionService.getUpdateEvent().add(this::subscriptionUpdate);
        executedListener = pipelineExecutorService.getOnExecuted().add(this::checkForIntervalExecution);
        registerFunction(DummyFunction.class, "Error:fallback");
        configRepository.findAll().forEach(this::load);
        pipelines.values()
                .stream()
                .filter(p -> p
                        .getConfig()
                        .getStartConditions()
                        .stream()
                        .anyMatch(c -> c.getTrigger() == TriggerType.OnStart))
                .forEach(pipelineExecutorService::triggerExecute);
    }

    private void subscriptionUpdate(Subscription subscription) {

        pipelines.values().forEach(pipeline -> {
            var matchedConditions = pipeline.getConfig().getStartConditions().stream().filter(c -> c.getTrigger() == TriggerType.OnSubscriptionUpdate).toList();
            if (matchedConditions.isEmpty()) {
                return;
            }

            matchedConditions.forEach(c -> {
                if (c.isDontStartIfRunning() && pipeline.isRunning()) {
                    return;
                }

                var properties = c.getProperties();
                var names = properties.getOrDefault("names", null);
                if (names == null || names.isEmpty()) {
                    pipelineExecutorService.triggerExecute(pipeline);
                    return;
                }

                if (Arrays.stream(names.split(Pattern.quote(","))).anyMatch(s -> subscription.getName().equals(s))) {
                    pipelineExecutorService.triggerExecute(pipeline);
                }
            });
        });

    }

    @PreDestroy
    private void destroy() {
        if (executedListener != null) {
            pipelineExecutorService.getOnExecuted().remove(executedListener);
            executedListener = null;
        }
        if (subscriptionUpdateListener != null) {
            subscriptionService.getUpdateEvent().remove(subscriptionUpdateListener);
            subscriptionUpdateListener = null;
        }
    }

}
