package p.studio.execution;

import p.studio.shipper.StudioShipperLogEntry;
import p.studio.utilities.events.EventSubscription;

import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;

public final class StudioExecutionSessionService {
    private final Object monitor = new Object();
    private final ArrayList<StudioExecutionLogEntry> logs = new ArrayList<>();
    private final CopyOnWriteArrayList<Consumer<StudioExecutionSnapshot>> listeners = new CopyOnWriteArrayList<>();
    private StudioExecutionState state = StudioExecutionState.IDLE;
    private long nextSequence = 1L;

    public StudioExecutionSnapshot snapshot() {
        synchronized (monitor) {
            return snapshotLocked();
        }
    }

    public EventSubscription subscribe(final Consumer<StudioExecutionSnapshot> listener) {
        final Consumer<StudioExecutionSnapshot> safeListener = Objects.requireNonNull(listener, "listener");
        listeners.add(safeListener);
        safeListener.accept(snapshot());
        return () -> listeners.remove(safeListener);
    }

    public void beginPreparationCycle() {
        publish(transitionLocked(StudioExecutionState.PREPARING, true));
    }

    public void transitionToPrepareFailed() {
        publish(transitionLocked(StudioExecutionState.PREPARE_FAILED, false));
    }

    public void transitionToConnecting() {
        publish(transitionLocked(StudioExecutionState.CONNECTING, false));
    }

    public void transitionToRunning() {
        publish(transitionLocked(StudioExecutionState.RUNNING, false));
    }

    public void transitionToRuntimeFailed() {
        publish(transitionLocked(StudioExecutionState.RUNTIME_FAILED, false));
    }

    public void transitionToStopped() {
        publish(transitionLocked(StudioExecutionState.STOPPED, false));
    }

    public void resetToIdle() {
        publish(transitionLocked(StudioExecutionState.IDLE, false));
    }

    public void appendShipperLog(final StudioShipperLogEntry entry) {
        final StudioShipperLogEntry safeEntry = Objects.requireNonNull(entry, "entry");
        appendLog(
                StudioExecutionLogSource.fromShipper(safeEntry.source()),
                StudioExecutionLogSeverity.fromShipper(safeEntry.level()),
                safeEntry.message(),
                safeEntry.timestamp());
    }

    public void appendRuntimeLog(
            final StudioExecutionLogSeverity severity,
            final String message) {
        appendLog(StudioExecutionLogSource.RUNTIME, severity, message, Instant.now());
    }

    public void appendRuntimeLog(
            final StudioExecutionLogSeverity severity,
            final String message,
            final Instant timestamp) {
        appendLog(StudioExecutionLogSource.RUNTIME, severity, message, timestamp);
    }

    public void appendLog(
            final StudioExecutionLogSource source,
            final StudioExecutionLogSeverity severity,
            final String message,
            final Instant timestamp) {
        final StudioExecutionSnapshot snapshot;
        synchronized (monitor) {
            final StudioExecutionLogEntry entry = new StudioExecutionLogEntry(
                    nextSequence++,
                    timestamp,
                    Objects.requireNonNull(source, "source"),
                    Objects.requireNonNull(severity, "severity"),
                    message);
            logs.add(entry);
            snapshot = snapshotLocked();
        }
        publish(snapshot);
    }

    private StudioExecutionSnapshot transitionLocked(
            final StudioExecutionState targetState,
            final boolean clearLogs) {
        final StudioExecutionSnapshot snapshot;
        synchronized (monitor) {
            final StudioExecutionState next = Objects.requireNonNull(targetState, "targetState");
            ensureTransitionAllowed(state, next);
            state = next;
            if (clearLogs) {
                logs.clear();
                nextSequence = 1L;
            }
            snapshot = snapshotLocked();
        }
        return snapshot;
    }

    private StudioExecutionSnapshot snapshotLocked() {
        return new StudioExecutionSnapshot(state, List.copyOf(logs));
    }

    private void ensureTransitionAllowed(
            final StudioExecutionState currentState,
            final StudioExecutionState nextState) {
        if (currentState == nextState) {
            return;
        }
        final boolean allowed = switch (currentState) {
            case IDLE -> nextState == StudioExecutionState.PREPARING;
            case PREPARING -> nextState == StudioExecutionState.PREPARE_FAILED
                    || nextState == StudioExecutionState.CONNECTING;
            case PREPARE_FAILED -> nextState == StudioExecutionState.IDLE
                    || nextState == StudioExecutionState.PREPARING;
            case CONNECTING -> nextState == StudioExecutionState.RUNNING
                    || nextState == StudioExecutionState.RUNTIME_FAILED
                    || nextState == StudioExecutionState.STOPPED;
            case RUNNING -> nextState == StudioExecutionState.STOPPED
                    || nextState == StudioExecutionState.RUNTIME_FAILED;
            case RUNTIME_FAILED -> nextState == StudioExecutionState.IDLE
                    || nextState == StudioExecutionState.PREPARING;
            case STOPPED -> nextState == StudioExecutionState.IDLE
                    || nextState == StudioExecutionState.PREPARING;
        };
        if (!allowed) {
            throw new IllegalStateException("illegal execution transition: " + currentState + " -> " + nextState);
        }
    }

    private void publish(final StudioExecutionSnapshot snapshot) {
        for (final Consumer<StudioExecutionSnapshot> listener : listeners) {
            listener.accept(snapshot);
        }
    }
}
