/*-
 * ~~~~~~licensing~~~~~~
 * cellar-extensions
 * ==========
 * Copyright (C) 2020 - 2025 EmDev LLC
 * ==========
 * You may not use this file except in accordance with the License Terms of the Copyright
 * Holder located at: https://entaxy.ru/eula . All copyrights, all intellectual property
 * rights to the Software and any copies are the property of the Copyright Holder. Unless
 * it is explicitly allowed the Copyright Holder, the User is prohibited from using the
 * Software for commercial purposes to provide services to third parties.
 * 
 * The Copyright Holder hereby declares that the Software is provided on an "AS IS".
 * Under no circumstances does the Copyright Holder guarantee or promise that the
 * Software provided by him will be suitable or not suitable for the specific purposes
 * of the User, that the Software will meet all commercial and personal subjective
 * expectations of the User, that the Software will work properly, without technical
 * errors, quickly and uninterruptedly.
 * 
 * Under no circumstances shall the Copyright Holder or its Affiliates is not liable
 * to the User for any direct or indirect losses of the User, his expenses or actual
 * damage, including, downtime; loss of bussines; lost profit; lost earnings; loss
 * or damage to data, property, etc.
 * ~~~~~~/licensing~~~~~~
 */
package ru.entaxy.platform.runtime.cellar.sequence.process;

import java.util.ArrayDeque;
import java.util.Collections;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.apache.karaf.cellar.core.event.Event;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ru.entaxy.platform.runtime.cellar.helper.Services;
import ru.entaxy.platform.runtime.cellar.sequence.CellarSequenceEvent;
import ru.entaxy.platform.runtime.cellar.sequence.CellarSequenceItemEvent;

public class SequenceTask implements Runnable, ChangeListener.CallBack {

    private static final Logger LOG = LoggerFactory.getLogger(SequenceTask.class);

    protected enum EVENT_RESULT {
        NONE,
        SUCCESS,
        ERROR,
        TIMEOUT
    }

    protected CellarSequenceEvent sequence;

    protected ArrayDeque<CellarSequenceItemEvent> events;

    protected Object eventsLock = new Object();

    protected CellarSequenceItemEvent currentEvent;

    protected ChangeListener currentChangeListener;

    protected Object currentChangeListenerLock = new Object();

    protected ExecutorService threadPool = Executors.newCachedThreadPool();

    protected Future<Object> future;

    protected Object futureLock = new Object();

    protected EVENT_RESULT currentResult;

    protected Object currentResultLock = new Object();

    public SequenceTask(CellarSequenceEvent event, ClassLoader classLoader) {
        super();
        this.sequence = event;
        events = new ArrayDeque<>(this.sequence.getSequence());
        Thread.currentThread().setContextClassLoader(classLoader);
        LOG.debug("\n--> CREATED SequenceTask for [{}]", sequence.getId());
    }

    protected void updateCurrentResult(EVENT_RESULT newResult) {
        synchronized (currentResultLock) {
            LOG.debug("\n--> CURRENT RESULT set to [{}]", newResult.name());
            this.currentResult = newResult;
        }
    }

    protected void resetResult() {
        updateCurrentResult(EVENT_RESULT.NONE);
    }

    @Override
    public void run() {

        LOG.debug("\n--> RUNNING SequenceTask for [{}]", sequence.getId());

        resetResult();

        // global limit for operation = 5 min
        long maxExecution = 300000;
        long executionStep = 500;

        while (!events.isEmpty()) {
            next();
            long executionPeriod = 0;
            while (EVENT_RESULT.NONE.equals(currentResult) && (executionPeriod < maxExecution)) {
                try {
                    LOG.debug("\n--> WAITING FOR RESULT  [{}]", currentEvent == null ? "null" : currentEvent.getId());
                    executionPeriod += executionStep;
                    Thread.sleep(executionStep);
                } catch (InterruptedException e) {
                    LOG.debug("\n--> INTERRUPTED WAITING FOR RESULT  [{}]",
                            currentEvent == null ? "null" : currentEvent.getId());
                    updateCurrentResult(EVENT_RESULT.ERROR);
                }
            }

            // maxExecution reached
            if (EVENT_RESULT.NONE.equals(currentResult)) {
                LOG.debug("\n--> CURRENT RESULT is NONE for [{}]",
                        currentEvent == null ? "null" : currentEvent.getId());
                updateCurrentResult(EVENT_RESULT.ERROR);
            }

            if (EVENT_RESULT.SUCCESS.equals(currentResult)) {
                LOG.debug("\n--> CURRENT RESULT is SUCCESS for [{}]",
                        currentEvent == null ? "null" : currentEvent.getId());
                resetResult();
            } else {
                LOG.debug("\n--> CURRENT RESULT is [{}] for [{}]",
                        currentResult == null ? "null" : currentResult.name(),
                        currentEvent == null ? "null" : currentEvent.getId());
                LOG.error("Failed processing sequence event");
                return;
            }
        }

    }

    protected boolean haveToWait() {
        if (events.isEmpty() && !sequence.isWaitLast())
            return false;
        return true;
    }

    protected void next() {
        synchronized (eventsLock) {
            LOG.debug("\n--> NEXT event");
            currentEvent = null;
            currentChangeListener = null;

            if (events.isEmpty())
                return;

            currentEvent = events.pop();
            LOG.debug("\n--> CURRENT event is [{}]", currentEvent == null ? "null" : currentEvent.getId());

            if (haveToWait()) {

                LOG.debug("\n--> WILL WAIT for result for [{}]", currentEvent == null ? "null" : currentEvent.getId());

                currentChangeListener = ChangeListenerFactory.create(currentEvent.getEvent());
                if (currentChangeListener == null) {
                    updateCurrentResult(EVENT_RESULT.ERROR);
                    return;
                }

                currentChangeListener.setCallback(this);
                currentChangeListener.register();

                Callable<Object> task = new Callable<Object>() {

                    @Override
                    public Object call() throws Exception {
                        Event eventToSend = currentEvent.getEvent();
                        eventToSend.setDestination(Collections.singleton(Services.INSTANCE.clusterManager.getNode()));

                        LOG.debug("\n--> EVENT SENT: [{}]:[{}]", eventToSend.getClass().getName(), eventToSend.getId());
                        Services.INSTANCE.eventProducer.produce(eventToSend);

                        // timeout for operation
                        // Thread.sleep(100000);
                        return null;
                    }
                };

                future = threadPool.submit(task);
                try {
                    Object result = future.get(100, TimeUnit.SECONDS);
                    LOG.debug("\n--> RESULT RECEIVED for [{}]", currentEvent == null ? "null" : currentEvent.getId());
                } catch (TimeoutException timeoutEx) {
                    LOG.debug("\n--> TimeoutException for [{}]", currentEvent == null ? "null" : currentEvent.getId());
                    updateCurrentResult(EVENT_RESULT.TIMEOUT);
                    unregisterChangeListener();
                } catch (InterruptedException ignore) {
                    // NOOP
                    LOG.debug("\n--> InterruptedException for [{}]",
                            currentEvent == null ? "null" : currentEvent.getId());
                } catch (CancellationException ignore) {
                    // NOOP
                    LOG.debug("\n--> CancellationException for [{}]",
                            currentEvent == null ? "null" : currentEvent.getId());
                } catch (Exception e) {
                    LOG.debug("\n--> Exception for [{}]", currentEvent == null ? "null" : currentEvent.getId());
                    LOG.error("Exception on next event processing", e);
                    updateCurrentResult(EVENT_RESULT.ERROR);
                    unregisterChangeListener();
                } finally {
                    // unregisterChangeListener();
                }
            } else {
                LOG.debug("\n--> WILL NOT WAIT for result for [{}]",
                        currentEvent == null ? "null" : currentEvent.getId());

                threadPool.execute(new Runnable() {

                    @Override
                    public void run() {
                        Event eventToSend = currentEvent.getEvent();
                        eventToSend.setDestination(Collections.singleton(Services.INSTANCE.clusterManager.getNode()));
                        Services.INSTANCE.eventProducer.produce(eventToSend);
                        LOG.debug("\n--> EVENT SENT: [{}]:[{}]", eventToSend.getClass().getName(), eventToSend.getId());
                    }
                });
                updateCurrentResult(EVENT_RESULT.SUCCESS);
            }
        }
    }

    protected void unregisterChangeListener() {
        synchronized (currentChangeListenerLock) {
            if (currentChangeListener != null)
                currentChangeListener.unregister();
        }
    }

    @Override
    public void success() {
        LOG.debug("\n--> CALLED 'success' for [{}]", currentEvent == null ? "null" : currentEvent.getId());
        synchronized (futureLock) {
            if (this.future != null)
                future.cancel(true);
        }
        unregisterChangeListener();
        updateCurrentResult(EVENT_RESULT.SUCCESS);
    }

    @Override
    public void timeout() {
        LOG.debug("\n--> CALLED 'timeout' for [{}]", currentEvent == null ? "null" : currentEvent.getId());
        synchronized (futureLock) {
            if (this.future != null)
                future.cancel(true);
        }
        unregisterChangeListener();
        updateCurrentResult(EVENT_RESULT.TIMEOUT);
    }

    @Override
    public void fail() {
        LOG.debug("\n--> CALLED 'fail' for [{}]", currentEvent == null ? "null" : currentEvent.getId());
        synchronized (futureLock) {
            if (this.future != null)
                future.cancel(true);
        }
        unregisterChangeListener();
        updateCurrentResult(EVENT_RESULT.ERROR);
    }

}
