package p.studio.execution;

import p.studio.Container;
import p.studio.StudioBackgroundTasks;
import p.studio.controls.shell.StudioActivityEntrySeverity;
import p.studio.debug.runtime.StudioRuntimeDebugConnectionSettings;
import p.studio.debug.runtime.StudioRuntimeHandshakeClient;
import p.studio.debug.runtime.StudioRuntimeHandshakeResult;
import p.studio.execution.runtime.StudioExternalRuntimeProcessLauncher;
import p.studio.execution.runtime.StudioRuntimeProcessHandle;
import p.studio.execution.runtime.StudioRuntimeProcessLauncher;
import p.studio.lsp.events.StudioExecutionLifecycleEvent;
import p.studio.projectstate.ProjectLocalStudioSetup;
import p.studio.projects.ProjectReference;
import p.studio.shipper.StudioShipperService;
import p.studio.workspaces.WorkspaceId;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import java.util.function.Consumer;

public final class StudioPlayStopCoordinator {
    private final Object monitor = new Object();
    private final ProjectReference projectReference;
    private final ProjectLocalStudioSetup projectSetup;
    private final StudioExecutionSessionService executionSession;
    private final StudioShipperPreparationRunner shipperRunner;
    private final StudioRuntimeHandshakeClient handshakeClient;
    private final StudioRuntimeProcessLauncher runtimeProcessLauncher;
    private final StudioBackgroundTasks backgroundTasks;
    private final Consumer<WorkspaceId> workspaceSelector;
    private final Consumer<StudioExecutionLifecycleEvent> lifecyclePublisher;
    private final boolean preflightRuntime;
    private StudioRuntimeProcessHandle activeProcess;
    private boolean stopRequested;

    public StudioPlayStopCoordinator(
            final ProjectReference projectReference,
            final ProjectLocalStudioSetup projectSetup,
            final StudioExecutionSessionService executionSession,
            final Consumer<WorkspaceId> workspaceSelector) {
        this(
                projectReference,
                projectSetup,
                executionSession,
                (project, sink) -> new StudioShipperService().prepare(project, sink),
                new p.studio.debug.runtime.StudioRuntimeHandshakeService(),
                new StudioExternalRuntimeProcessLauncher(),
                Container.backgroundTasks(),
                workspaceSelector,
                event -> Container.eventBus().publish(event),
                true);
    }

    StudioPlayStopCoordinator(
            final ProjectReference projectReference,
            final ProjectLocalStudioSetup projectSetup,
            final StudioExecutionSessionService executionSession,
            final StudioShipperPreparationRunner shipperRunner,
            final StudioRuntimeHandshakeClient handshakeClient,
            final StudioRuntimeProcessLauncher runtimeProcessLauncher,
            final StudioBackgroundTasks backgroundTasks,
            final Consumer<WorkspaceId> workspaceSelector,
            final Consumer<StudioExecutionLifecycleEvent> lifecyclePublisher,
            final boolean preflightRuntime) {
        this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
        this.projectSetup = Objects.requireNonNull(projectSetup, "projectSetup");
        this.executionSession = Objects.requireNonNull(executionSession, "executionSession");
        this.shipperRunner = Objects.requireNonNull(shipperRunner, "shipperRunner");
        this.handshakeClient = Objects.requireNonNull(handshakeClient, "handshakeClient");
        this.runtimeProcessLauncher = Objects.requireNonNull(runtimeProcessLauncher, "runtimeProcessLauncher");
        this.backgroundTasks = Objects.requireNonNull(backgroundTasks, "backgroundTasks");
        this.workspaceSelector = Objects.requireNonNull(workspaceSelector, "workspaceSelector");
        this.lifecyclePublisher = Objects.requireNonNull(lifecyclePublisher, "lifecyclePublisher");
        this.preflightRuntime = preflightRuntime;
    }

    public void play() {
        synchronized (monitor) {
            if (isBusyState(executionSession.snapshot().state())) {
                return;
            }
            stopRequested = false;
        }
        workspaceSelector.accept(WorkspaceId.DEBUG);
        executionSession.beginPreparationCycle();
        publishLifecycle("Play", "Play started", StudioActivityEntrySeverity.INFO, false);
        backgroundTasks.submit(this::runPlayFlow);
    }

    public void stop() {
        final StudioExecutionState state = executionSession.snapshot().state();
        if (state == StudioExecutionState.PREPARING) {
            return;
        }
        final StudioRuntimeProcessHandle processToStop;
        synchronized (monitor) {
            stopRequested = true;
            processToStop = activeProcess;
            activeProcess = null;
        }
        handshakeClient.disconnect(executionSession);
        if (processToStop != null && processToStop.isAlive()) {
            processToStop.destroyForcibly();
        }
        publishLifecycle("Play", "Runtime stop requested", StudioActivityEntrySeverity.INFO, false);
    }

    public void shutdown() {
        stop();
    }

    private void runPlayFlow() {
        final var prepareResult = shipperRunner.prepare(projectReference, executionSession::appendShipperLog);
        if (!prepareResult.success()) {
            executionSession.transitionToPrepareFailed();
            publishLifecycle("Play", "Preparation failed", StudioActivityEntrySeverity.ERROR, true);
            return;
        }

        executionSession.transitionToConnecting();
        final String runtimePath = validateRuntimePath(preflightRuntime);
        if (runtimePath == null) {
            executionSession.transitionToRuntimeFailed();
            publishLifecycle("Play", "Runtime preflight failed", StudioActivityEntrySeverity.ERROR, true);
            return;
        }

        final StudioRuntimeProcessHandle processHandle;
        try {
            processHandle = runtimeProcessLauncher.launch(projectReference, runtimePath);
        } catch (IOException ioException) {
            executionSession.appendRuntimeLog(StudioExecutionLogSeverity.ERROR, "Runtime preflight failed: " + ioException.getMessage());
            executionSession.transitionToRuntimeFailed();
            publishLifecycle("Play", "Runtime spawn failed", StudioActivityEntrySeverity.ERROR, true);
            return;
        }

        synchronized (monitor) {
            activeProcess = processHandle;
            stopRequested = false;
        }
        streamProcessOutput(processHandle.stdout(), StudioExecutionLogSeverity.INFO);
        streamProcessOutput(processHandle.stderr(), StudioExecutionLogSeverity.ERROR);
        processHandle.onExit().thenAccept(exitCode -> handleProcessExit(processHandle, exitCode));

        executionSession.transitionToConnecting();
        final StudioRuntimeHandshakeResult handshake = handshakeClient.connectWithRetry(
                executionSession,
                StudioRuntimeDebugConnectionSettings.defaults(),
                20,
                250L);
        if (!handshake.success()) {
            processHandle.destroyForcibly();
            synchronized (monitor) {
                if (activeProcess == processHandle) {
                    activeProcess = null;
                }
            }
            publishLifecycle("Debug", "Runtime handshake failed", StudioActivityEntrySeverity.ERROR, true);
            return;
        }
        publishLifecycle("Debug", "Runtime started", StudioActivityEntrySeverity.SUCCESS, false);
    }

    private void handleProcessExit(
            final StudioRuntimeProcessHandle processHandle,
            final int exitCode) {
        final boolean wasStopRequested;
        synchronized (monitor) {
            if (activeProcess == processHandle) {
                activeProcess = null;
            }
            wasStopRequested = stopRequested;
        }
        if (wasStopRequested || executionSession.snapshot().state() == StudioExecutionState.STOPPED) {
            executionSession.appendRuntimeLog(StudioExecutionLogSeverity.INFO, "Runtime process stopped.");
            publishLifecycle("Debug", "Runtime stopped", StudioActivityEntrySeverity.INFO, false);
            return;
        }
        if (exitCode == 0) {
            executionSession.appendRuntimeLog(StudioExecutionLogSeverity.INFO, "Runtime process exited successfully.");
            executionSession.transitionToStopped();
            publishLifecycle("Debug", "Runtime finished", StudioActivityEntrySeverity.SUCCESS, false);
            return;
        }
        executionSession.appendRuntimeLog(StudioExecutionLogSeverity.ERROR, "Runtime process exited with code " + exitCode + ".");
        executionSession.transitionToRuntimeFailed();
        publishLifecycle("Debug", "Runtime failed", StudioActivityEntrySeverity.ERROR, true);
    }

    private void streamProcessOutput(
            final InputStream stream,
            final StudioExecutionLogSeverity severity) {
        backgroundTasks.submit(() -> {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    executionSession.appendRuntimeLog(severity, line);
                }
            } catch (IOException ignored) {
            }
        });
    }

    private String validateRuntimePath(final boolean preflightRuntime) {
        final String runtimePathValue = projectSetup.prometeuRuntimePath();
        if (runtimePathValue == null) {
            executionSession.appendRuntimeLog(StudioExecutionLogSeverity.ERROR, "Runtime preflight failed: prometeuRuntimePath is missing.");
            return null;
        }
        if (!preflightRuntime) {
            return runtimePathValue;
        }
        final Path runtimePath = Path.of(runtimePathValue).resolve("prometeu").toAbsolutePath().normalize();
        if (!Files.isRegularFile(runtimePath)) {
            executionSession.appendRuntimeLog(StudioExecutionLogSeverity.ERROR, "Runtime preflight failed: runtime not found at " + runtimePath + ".");
            return null;
        }
        if (!Files.isExecutable(runtimePath)) {
            executionSession.appendRuntimeLog(StudioExecutionLogSeverity.ERROR, "Runtime preflight failed: runtime is not executable at " + runtimePath + ".");
            return null;
        }
        return runtimePath.toString();
    }

    private void publishLifecycle(
            final String source,
            final String message,
            final StudioActivityEntrySeverity severity,
            final boolean sticky) {
        lifecyclePublisher.accept(new StudioExecutionLifecycleEvent(source, message, severity, sticky));
    }

    private boolean isBusyState(final StudioExecutionState state) {
        return state == StudioExecutionState.PREPARING
                || state == StudioExecutionState.CONNECTING
                || state == StudioExecutionState.RUNNING;
    }
}
