package vassistent.service;

import vassistent.util.Logger;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Properties;
import java.util.function.BooleanSupplier;

/**
 * Starts and stops optional external helper processes used by the application.
 */
public class ProcessManagerService {

    /**
     * Represents startup status for a single managed process component.
     */
    public static final class ProcessStartStatus {

        private final String componentName;
        private final boolean enabled;
        private final boolean started;
        private final String errorMessage;

        /**
         * Creates an immutable process start status instance.
         *
         * @param componentName logical component name
         * @param enabled whether component startup was enabled
         * @param started whether startup completed without launch error
         * @param errorMessage optional startup error details
         */
        private ProcessStartStatus(
                String componentName,
                boolean enabled,
                boolean started,
                String errorMessage
        ) {
            this.componentName = componentName;
            this.enabled = enabled;
            this.started = started;
            this.errorMessage = errorMessage;
        }

        /**
         * Creates a disabled status for a component.
         *
         * @param componentName logical component name
         * @return disabled status instance
         */
        static ProcessStartStatus disabled(String componentName) {
            return new ProcessStartStatus(componentName, false, false, null);
        }

        /**
         * Creates a successful startup status for a component.
         *
         * @param componentName logical component name
         * @return successful status instance
         */
        static ProcessStartStatus started(String componentName) {
            return new ProcessStartStatus(componentName, true, true, null);
        }

        /**
         * Creates a failed startup status for a component.
         *
         * @param componentName logical component name
         * @param errorMessage startup failure details
         * @return failure status instance
         */
        static ProcessStartStatus failed(String componentName, String errorMessage) {
            return new ProcessStartStatus(componentName, true, false, errorMessage);
        }

        /**
         * Returns whether startup failed for an enabled component.
         *
         * @return {@code true} when startup failed
         */
        public boolean hasFailed() {
            return enabled && !started;
        }

        /**
         * Returns logical component name.
         *
         * @return component name
         */
        public String getComponentName() {
            return componentName;
        }

        /**
         * Returns whether startup was enabled.
         *
         * @return enabled flag
         */
        public boolean isEnabled() {
            return enabled;
        }

        /**
         * Returns whether startup completed without launch error.
         *
         * @return started flag
         */
        public boolean isStarted() {
            return started;
        }

        /**
         * Returns startup error details when available.
         *
         * @return error details or {@code null}
         */
        public String getErrorMessage() {
            return errorMessage;
        }
    }

    /**
     * Aggregates startup status for all managed process components.
     */
    public static final class StartupReport {

        private final ProcessStartStatus mqttSimulator;
        private final ProcessStartStatus unreal;

        /**
         * Creates a startup report.
         *
         * @param mqttSimulator startup status of MQTT simulator component
         * @param unreal startup status of Unreal launcher component
         */
        StartupReport(
                ProcessStartStatus mqttSimulator,
                ProcessStartStatus unreal
        ) {
            this.mqttSimulator = Objects.requireNonNull(mqttSimulator);
            this.unreal = Objects.requireNonNull(unreal);
        }

        /**
         * Returns startup status of MQTT simulator component.
         *
         * @return MQTT simulator startup status
         */
        public ProcessStartStatus getMqttSimulator() {
            return mqttSimulator;
        }

        /**
         * Returns startup status of Unreal launcher component.
         *
         * @return Unreal startup status
         */
        public ProcessStartStatus getUnreal() {
            return unreal;
        }

        /**
         * Returns whether at least one enabled process failed to launch.
         *
         * @return {@code true} when startup has failures
         */
        public boolean hasFailures() {
            return mqttSimulator.hasFailed() || unreal.hasFailed();
        }

        /**
         * Builds a compact startup failure message suitable for exceptions.
         *
         * @return failure message
         */
        public String buildFailureMessage() {
            StringBuilder message =
                    new StringBuilder("External process startup failed:");

            appendFailure(message, mqttSimulator);
            appendFailure(message, unreal);

            return message.toString();
        }

        /**
         * Appends one failed component summary to a message builder.
         *
         * @param message target message builder
         * @param status component startup status
         */
        private static void appendFailure(
                StringBuilder message,
                ProcessStartStatus status
        ) {
            if (!status.hasFailed()) {
                return;
            }

            message.append(System.lineSeparator())
                    .append("- ")
                    .append(status.getComponentName())
                    .append(": ");

            String error = status.getErrorMessage();
            if (error == null || error.isBlank()) {
                message.append("unknown startup error");
            } else {
                message.append(error);
            }
        }
    }

    @FunctionalInterface
    interface ProcessLauncher {
        Process launch(ProcessBuilder processBuilder) throws IOException;
    }

    private static final String DEFAULT_UNREAL_PID_FILE =
            "C:\\Users\\Student\\Documents\\Dannick\\avatar\\unreal.pid";

    private static final String DEFAULT_SIGNALLING_PID_FILE =
            "C:\\Users\\Student\\Documents\\Dannick\\avatar\\signalling.pid";

    private static final boolean DEFAULT_STARTUP_HEALTHCHECK_ENABLED = true;
    private static final long DEFAULT_STARTUP_HEALTHCHECK_TIMEOUT_MILLIS = 10_000L;
    private static final long DEFAULT_STARTUP_HEALTHCHECK_POLL_MILLIS = 250L;

    private final Properties config;
    private final ProcessLauncher processLauncher;
    private final String unrealPidFile;
    private final String signallingPidFile;
    private final boolean startupHealthcheckEnabled;
    private final long startupHealthcheckTimeoutMillis;
    private final long startupHealthcheckPollMillis;

    private Process pythonProcess;
    private Process unrealProcess;

    /**
     * Creates a process manager using runtime configuration values.
     *
     * @param config loaded application properties
     */
    public ProcessManagerService(Properties config) {
        this(
                config,
                ProcessBuilder::start,
                cleanConfigValue(
                        config.getProperty(
                                "unreal.pid.file",
                                DEFAULT_UNREAL_PID_FILE
                        )
                ),
                cleanConfigValue(
                        config.getProperty(
                                "unreal.signalling.pid.file",
                                DEFAULT_SIGNALLING_PID_FILE
                        )
                )
        );
    }

    /**
     * Creates a process manager with injectable process launcher and PID file paths.
     *
     * @param config loaded application properties
     * @param processLauncher strategy used to launch process builders
     * @param unrealPidFile PID file path for Unreal process cleanup
     * @param signallingPidFile PID file path for signalling process cleanup
     */
    ProcessManagerService(
            Properties config,
            ProcessLauncher processLauncher,
            String unrealPidFile,
            String signallingPidFile
    ) {
        this.config = Objects.requireNonNull(config);
        this.processLauncher = Objects.requireNonNull(processLauncher);
        this.unrealPidFile = Objects.requireNonNull(unrealPidFile);
        this.signallingPidFile = Objects.requireNonNull(signallingPidFile);
        this.startupHealthcheckEnabled = Boolean.parseBoolean(
                config.getProperty(
                        "process.startup.healthcheck.enabled",
                        String.valueOf(DEFAULT_STARTUP_HEALTHCHECK_ENABLED)
                )
        );
        this.startupHealthcheckTimeoutMillis = readPositiveLong(
                config.getProperty("process.startup.healthcheck.timeout.millis"),
                DEFAULT_STARTUP_HEALTHCHECK_TIMEOUT_MILLIS
        );
        this.startupHealthcheckPollMillis = readPositiveLong(
                config.getProperty("process.startup.healthcheck.poll.millis"),
                DEFAULT_STARTUP_HEALTHCHECK_POLL_MILLIS
        );
    }

    /**
     * Starts optional helper processes according to configuration flags.
     *
     * @return startup status report for all managed components
     */
    public StartupReport startProcesses() {

        ProcessStartStatus mqttSimulator = startPythonIfEnabled();
        ProcessStartStatus unreal = startUnrealIfEnabled();

        return new StartupReport(mqttSimulator, unreal);
    }

     /**
     * Starts the Python MQTT simulator if enabled.
     *
     * @return startup status for the simulator component
     */
    private ProcessStartStatus startPythonIfEnabled() {

        boolean enabled = Boolean.parseBoolean(
                config.getProperty("mqtt_sim.enabled", "false")
        );

        if (!enabled) {
            return ProcessStartStatus.disabled("mqtt_sim");
        }

        try {
            String script = cleanConfigValue(config.getProperty("mqtt_sim.script"));
            String python = cleanConfigValue(config.getProperty("python.path"));

            ProcessBuilder pb = new ProcessBuilder(python, script);
            pb.redirectErrorStream(true);
            configureMqttSimulatorEnvironment(pb);

            pythonProcess = processLauncher.launch(pb);
            verifyPythonStartupHealth();

            Logger.info("PROCESS", "MQTT simulator started");
            return ProcessStartStatus.started("mqtt_sim");
        } catch (Exception e) {
            if (e instanceof InterruptedException) {
                Thread.currentThread().interrupt();
            }
            Logger.error("PROCESS", "MQTT simulator startup failed", e);
            return ProcessStartStatus.failed(
                    "mqtt_sim",
                    exceptionMessage(e)
            );
        }
    }

     /**
     * Starts Unreal-related processes if enabled.
     *
     * @return startup status for the Unreal component
     */
    private ProcessStartStatus startUnrealIfEnabled() {

        boolean enabled = Boolean.parseBoolean(config.getProperty("unreal.enabled", "false"));

        if (!enabled) {
            return ProcessStartStatus.disabled("unreal");
        }

        try {
            startUnrealEngine();
            verifyUnrealStartupHealth();
            return ProcessStartStatus.started("unreal");

        } catch (Exception e) {
            if (e instanceof InterruptedException) {
                Thread.currentThread().interrupt();
            }
            Logger.error("PROCESS", "Unreal startup failed", e);
            return ProcessStartStatus.failed(
                    "unreal",
                    exceptionMessage(e)
            );
        }
    }

    /**
     * Starts the configured Unreal executable script via PowerShell.
     *
     * @throws IOException if process startup fails
     */
    private void startUnrealEngine() throws IOException {

        String unrealPsScript =
                cleanConfigValue(config.getProperty("unreal.executable"));
        ProcessBuilder pb = new ProcessBuilder(
                "powershell.exe",
                "-ExecutionPolicy", "Bypass",
                "-File",
                unrealPsScript
        );

        configureUnrealScriptEnvironment(pb);
        pb.redirectErrorStream(true);

        unrealProcess = processLauncher.launch(pb);

        //pb.directory(new File(exe).getParentFile());

        Logger.info("PROCESS",
                "Unreal Engine started " + pb.command());
    }

    /**
     * Verifies that the launched MQTT simulator process stays alive shortly after startup.
     *
     * @throws InterruptedException if health-check waiting is interrupted
     */
    private void verifyPythonStartupHealth() throws InterruptedException {
        if (!startupHealthcheckEnabled) {
            return;
        }

        boolean started = waitUntil(() -> pythonProcess != null && pythonProcess.isAlive());
        if (!started) {
            throw new IllegalStateException(
                    "MQTT simulator startup health check failed: process is not running."
            );
        }
    }

    /**
     * Verifies Unreal startup by waiting for both PID files and checking whether those PIDs are alive.
     *
     * @throws InterruptedException if health-check waiting is interrupted
     */
    private void verifyUnrealStartupHealth() throws InterruptedException {
        if (!startupHealthcheckEnabled) {
            return;
        }

        boolean healthy = waitUntil(() ->
                isPidAlive(readPidFromFile(unrealPidFile))
                        && isPidAlive(readPidFromFile(signallingPidFile))
        );

        if (!healthy) {
            throw new IllegalStateException(buildUnrealHealthCheckFailureMessage());
        }
    }

    /**
     * Stops all managed processes and attempts cleanup based on known PID files.
     */
    public void shutdown() {

        Logger.info("PROCESS", "External process shutdown started");

        terminateProcess(pythonProcess);
        terminateProcess(unrealProcess);

        killProcessFromPidFile(unrealPidFile);
        killProcessFromPidFile(signallingPidFile);

        Logger.info("PROCESS", "External processes stopped");
    }

    /**
     * Terminates a process tree for a tracked process handle.
     *
     * @param process process handle to terminate; ignored when {@code null}
     */
    private void terminateProcess(Process process) {

        if (process == null)
            return;

        try {

            long pid = process.pid();

            ProcessBuilder pb = new ProcessBuilder(
                    "taskkill",
                    "/PID",
                    String.valueOf(pid),
                    "/T",
                    "/F"
            );

            processLauncher.launch(pb).waitFor();

            Logger.info("PROCESS",
                    "Process tree terminated -> PID " + pid);

        } catch (Exception e) {
            Logger.error("PROCESS",
                    "Failed to terminate process", e);
        }
    }

    /**
     * Terminates a process tree using a PID read from a file.
     *
     * @param file absolute path to PID file
     */
    private void killProcessFromPidFile(String file) {

        try {

            long pid = Long.parseLong(
                    Files.readString(Path.of(file)).trim()
            );

            ProcessBuilder pb = new ProcessBuilder(
                    "taskkill",
                    "/PID",
                    String.valueOf(pid),
                    "/T",
                    "/F"
            );

            processLauncher.launch(pb).waitFor();

        } catch (Exception e) {
            Logger.error("PROCESS", "PID kill failed", e);
        }
    }

    /**
     * Returns the currently tracked Python simulator process.
     *
     * @return Python process or {@code null}
     */
    Process getPythonProcess() {
        return pythonProcess;
    }

    /**
     * Returns the currently tracked Unreal process.
     *
     * @return Unreal process or {@code null}
     */
    Process getUnrealProcess() {
        return unrealProcess;
    }

    /**
     * Waits until a startup condition is satisfied or health-check timeout is reached.
     *
     * @param condition startup condition to evaluate
     * @return {@code true} when condition becomes true before timeout
     * @throws InterruptedException if waiting is interrupted
     */
    private boolean waitUntil(BooleanSupplier condition) throws InterruptedException {
        long deadline = System.currentTimeMillis() + startupHealthcheckTimeoutMillis;

        while (System.currentTimeMillis() <= deadline) {
            if (condition.getAsBoolean()) {
                return true;
            }
            sleepUntilNextPoll(deadline);
        }

        return condition.getAsBoolean();
    }

    /**
     * Sleeps until next health-check poll or until timeout deadline, whichever is sooner.
     *
     * @param deadlineMillis absolute timeout deadline in milliseconds
     * @throws InterruptedException if sleep is interrupted
     */
    private void sleepUntilNextPoll(long deadlineMillis) throws InterruptedException {
        long remaining = deadlineMillis - System.currentTimeMillis();
        if (remaining <= 0) {
            return;
        }

        long sleepMillis = Math.min(startupHealthcheckPollMillis, remaining);
        if (sleepMillis > 0) {
            Thread.sleep(sleepMillis);
        }
    }

    /**
     * Reads a PID from a file path when present and parseable.
     *
     * @param pidFile absolute PID file path
     * @return parsed PID value or {@code null} when unavailable
     */
    private static Long readPidFromFile(String pidFile) {
        try {
            if (pidFile == null || pidFile.isBlank()) {
                return null;
            }
            Path path = Path.of(pidFile);
            if (!Files.isRegularFile(path)) {
                return null;
            }
            return Long.parseLong(Files.readString(path).trim());
        } catch (Exception ignored) {
            return null;
        }
    }

    /**
     * Returns whether a PID maps to a currently alive process.
     *
     * @param pid process identifier
     * @return {@code true} when the process exists and is alive
     */
    private static boolean isPidAlive(Long pid) {
        if (pid == null || pid <= 0) {
            return false;
        }
        return ProcessHandle.of(pid)
                .map(ProcessHandle::isAlive)
                .orElse(false);
    }

    /**
     * Builds a detailed health-check failure message for Unreal startup diagnostics.
     *
     * @return failure message with PID file and liveness details
     */
    private String buildUnrealHealthCheckFailureMessage() {
        Long unrealPid = readPidFromFile(unrealPidFile);
        Long signallingPid = readPidFromFile(signallingPidFile);

        return "Unreal startup health check failed: "
                + describePidState("unreal", unrealPidFile, unrealPid)
                + "; "
                + describePidState("signalling", signallingPidFile, signallingPid);
    }

    /**
     * Describes PID-file and liveness state for one process component.
     *
     * @param componentName component identifier
     * @param pidFile configured PID file path
     * @param pid parsed PID value
     * @return human-readable state summary
     */
    private static String describePidState(
            String componentName,
            String pidFile,
            Long pid
    ) {
        if (pid == null) {
            return componentName + " PID file missing or invalid (" + pidFile + ")";
        }

        if (!isPidAlive(pid)) {
            return componentName + " PID " + pid + " is not running";
        }

        return componentName + " PID " + pid + " is running";
    }

    /**
     * Normalizes string values loaded from properties and strips optional quotes.
     *
     * @param value raw config value
     * @return normalized value or {@code null}
     */
    private static String cleanConfigValue(String value) {
        if (value == null) {
            return null;
        }

        String trimmed = value.trim();

        if ((trimmed.startsWith("\"") && trimmed.endsWith("\""))
                || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
            return trimmed.substring(1, trimmed.length() - 1);
        }

        return trimmed;
    }

    /**
     * Returns a compact message for startup exception reporting.
     *
     * @param exception startup exception
     * @return compact exception details
     */
    private static String exceptionMessage(Exception exception) {
        String type = exception.getClass().getSimpleName();
        String message = exception.getMessage();

        if (message == null || message.isBlank()) {
            return type;
        }

        return type + ": " + message;
    }

    /**
     * Parses an optional positive long value and falls back when absent or invalid.
     *
     * @param rawValue raw config value
     * @param fallback fallback value
     * @return parsed positive value or fallback
     */
    private static long readPositiveLong(String rawValue, long fallback) {
        String value = cleanConfigValue(rawValue);
        if (value == null || value.isBlank()) {
            return fallback;
        }

        try {
            long parsed = Long.parseLong(value);
            return parsed > 0 ? parsed : fallback;
        } catch (NumberFormatException e) {
            return fallback;
        }
    }

    /**
     * Passes optional simulator settings from app config to the Python process environment.
     *
     * @param processBuilder simulator process builder
     */
    private void configureMqttSimulatorEnvironment(ProcessBuilder processBuilder) {
        putEnvFromConfig(processBuilder, "mqtt_sim.broker", "MQTT_SIM_BROKER");
        putEnvFromConfig(processBuilder, "mqtt_sim.port", "MQTT_SIM_PORT");
        putEnvFromConfig(processBuilder, "mqtt_sim.topic", "MQTT_SIM_TOPIC");
        putEnvFromConfig(processBuilder, "mqtt_sim.username", "MQTT_SIM_USERNAME");
        putEnvFromConfig(processBuilder, "mqtt_sim.password", "MQTT_SIM_PASSWORD");
        putEnvFromConfig(processBuilder, "mqtt_sim.qos", "MQTT_SIM_QOS");
        putEnvFromConfig(processBuilder, "mqtt_sim.interval_seconds", "MQTT_SIM_INTERVAL_SECONDS");
        putEnvFromConfig(processBuilder, "mqtt_sim.start_id", "MQTT_SIM_START_ID");
        putEnvFromConfig(processBuilder, "mqtt_sim.client_id", "MQTT_SIM_CLIENT_ID");
        putEnvFromConfig(processBuilder, "mqtt_sim.log_file", "MQTT_SIM_LOG_FILE");
    }

    /**
     * Passes optional Unreal launcher settings from app config to the PowerShell process environment.
     *
     * @param processBuilder Unreal process builder
     */
    private void configureUnrealScriptEnvironment(ProcessBuilder processBuilder) {
        putEnvFromConfig(processBuilder, "unreal.target.executable", "VGA_UE_EXE");
        putEnvFromConfig(processBuilder, "unreal.target.args", "VGA_UE_ARGS");
        putEnvFromConfig(processBuilder, "unreal.target.working_dir", "VGA_UE_WORKDIR");
        putEnvFromConfig(processBuilder, "unreal.signalling_server.script", "VGA_SIGNALLING_BAT");
        putEnvFromConfig(processBuilder, "unreal.startup.delay.seconds", "VGA_SIGNALLING_DELAY");

        String pidDir = cleanConfigValue(config.getProperty("unreal.pid.dir"));
        if (pidDir == null || pidDir.isBlank()) {
            try {
                Path parent = Path.of(unrealPidFile).getParent();
                if (parent != null) {
                    pidDir = parent.toString();
                }
            } catch (Exception ignored) {
                pidDir = null;
            }
        }

        if (pidDir != null && !pidDir.isBlank()) {
            processBuilder.environment().put("VGA_PID_DIR", pidDir);
        }
    }

    /**
     * Adds a config value to process environment when present.
     *
     * @param processBuilder target process builder
     * @param configKey application properties key
     * @param envName environment variable name
     */
    private void putEnvFromConfig(
            ProcessBuilder processBuilder,
            String configKey,
            String envName
    ) {
        String value = cleanConfigValue(config.getProperty(configKey));
        if (value != null && !value.isBlank()) {
            processBuilder.environment().put(envName, value);
        }
    }
}
