// 
// Decompiled by Procyon v0.6.0
// 

package com.hypixel.hytale.server.core.update;

import com.hypixel.hytale.common.util.SystemUtil;
import com.hypixel.hytale.server.core.permissions.PermissionsModule;
import java.util.Iterator;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.ShutdownReason;
import java.awt.Color;
import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.server.core.universe.Universe;
import java.util.concurrent.CancellationException;
import com.hypixel.hytale.server.core.auth.ServerAuthManager;
import com.hypixel.hytale.server.core.Constants;
import com.hypixel.hytale.common.util.java.ManifestUtil;
import com.hypixel.hytale.server.core.HytaleServerConfig;
import java.util.concurrent.TimeUnit;
import com.hypixel.hytale.server.core.HytaleServer;
import com.hypixel.hytale.server.core.command.system.AbstractCommand;
import com.hypixel.hytale.server.core.update.command.UpdateCommand;
import java.util.logging.Level;
import java.util.concurrent.Executors;
import javax.annotation.Nonnull;
import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nullable;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledExecutorService;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.common.plugin.PluginManifest;
import com.hypixel.hytale.server.core.plugin.JavaPlugin;

public class UpdateModule extends JavaPlugin
{
    public static final PluginManifest MANIFEST;
    private static final HytaleLogger LOGGER;
    public static final boolean KILL_SWITCH_ENABLED;
    private static UpdateModule instance;
    private final ScheduledExecutorService scheduler;
    @Nullable
    private ScheduledFuture<?> updateCheckTask;
    @Nullable
    private ScheduledFuture<?> autoApplyTask;
    private final AtomicReference<UpdateService.VersionManifest> latestKnownVersion;
    private final AtomicReference<CompletableFuture<?>> activeDownload;
    private final AtomicReference<Thread> activeDownloadThread;
    private final AtomicBoolean downloadLock;
    private final AtomicLong downloadStartTime;
    private final AtomicLong downloadedBytes;
    private final AtomicLong totalBytes;
    private final AtomicLong autoApplyScheduledTime;
    private final AtomicLong lastWarningTime;
    
    public UpdateModule(@Nonnull final JavaPluginInit init) {
        super(init);
        this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
            final Thread t = new Thread(r, "UpdateChecker");
            t.setDaemon(true);
            return t;
        });
        this.latestKnownVersion = new AtomicReference<UpdateService.VersionManifest>();
        this.activeDownload = new AtomicReference<CompletableFuture<?>>();
        this.activeDownloadThread = new AtomicReference<Thread>();
        this.downloadLock = new AtomicBoolean(false);
        this.downloadStartTime = new AtomicLong(0L);
        this.downloadedBytes = new AtomicLong(0L);
        this.totalBytes = new AtomicLong(0L);
        this.autoApplyScheduledTime = new AtomicLong(0L);
        this.lastWarningTime = new AtomicLong(0L);
        UpdateModule.instance = this;
    }
    
    @Nullable
    public static UpdateModule get() {
        return UpdateModule.instance;
    }
    
    @Override
    protected void setup() {
        if (UpdateModule.KILL_SWITCH_ENABLED) {
            UpdateModule.LOGGER.at(Level.INFO).log("Update commands disabled via HYTALE_DISABLE_UPDATES environment variable");
        }
        this.getCommandRegistry().registerCommand(new UpdateCommand());
    }
    
    @Override
    protected void start() {
        if (UpdateModule.KILL_SWITCH_ENABLED) {
            return;
        }
        final String stagedVersion = UpdateService.getStagedVersion();
        if (stagedVersion != null) {
            this.logStagedUpdateWarning(stagedVersion, true);
            this.startAutoApplyTaskIfNeeded();
        }
        if (this.shouldEnableUpdateChecker()) {
            final HytaleServerConfig.UpdateConfig config = HytaleServer.get().getConfig().getUpdateConfig();
            final int intervalSeconds = config.getCheckIntervalSeconds();
            UpdateModule.LOGGER.at(Level.INFO).log("Update checker enabled (interval: %ds)", intervalSeconds);
            this.updateCheckTask = this.scheduler.scheduleAtFixedRate(this::performUpdateCheck, 60L, intervalSeconds, TimeUnit.SECONDS);
        }
    }
    
    private synchronized void startAutoApplyTaskIfNeeded() {
        if (this.autoApplyTask != null) {
            return;
        }
        final HytaleServerConfig.UpdateConfig config = HytaleServer.get().getConfig().getUpdateConfig();
        final HytaleServerConfig.UpdateConfig.AutoApplyMode autoApplyMode = config.getAutoApplyMode();
        if (autoApplyMode == HytaleServerConfig.UpdateConfig.AutoApplyMode.DISABLED) {
            return;
        }
        UpdateModule.LOGGER.at(Level.INFO).log("Starting auto-apply task (mode: %s, delay: %d min)", autoApplyMode, config.getAutoApplyDelayMinutes());
        this.autoApplyTask = this.scheduler.scheduleAtFixedRate(this::performAutoApplyCheck, 0L, 60L, TimeUnit.SECONDS);
    }
    
    @Override
    protected void shutdown() {
        if (this.updateCheckTask != null) {
            this.updateCheckTask.cancel(false);
        }
        if (this.autoApplyTask != null) {
            this.autoApplyTask.cancel(false);
        }
        this.scheduler.shutdown();
    }
    
    public void onServerReady() {
        if (UpdateModule.KILL_SWITCH_ENABLED) {
            return;
        }
        final String stagedVersion = UpdateService.getStagedVersion();
        if (stagedVersion != null) {
            this.logStagedUpdateWarning(stagedVersion, false);
        }
    }
    
    @Nullable
    public UpdateService.VersionManifest getLatestKnownVersion() {
        return this.latestKnownVersion.get();
    }
    
    public void setLatestKnownVersion(@Nullable final UpdateService.VersionManifest version) {
        this.latestKnownVersion.set(version);
    }
    
    public boolean isDownloadInProgress() {
        return this.downloadLock.get();
    }
    
    public boolean tryAcquireDownloadLock() {
        return this.downloadLock.compareAndSet(false, true);
    }
    
    public void setActiveDownload(@Nullable final CompletableFuture<?> download, @Nullable final Thread thread) {
        this.activeDownload.set(download);
        this.activeDownloadThread.set(thread);
    }
    
    public void releaseDownloadLock() {
        this.activeDownload.set(null);
        this.activeDownloadThread.set(null);
        this.downloadLock.set(false);
        this.downloadStartTime.set(0L);
        this.downloadedBytes.set(0L);
        this.totalBytes.set(0L);
    }
    
    public void updateDownloadProgress(final long downloaded, final long total) {
        if (this.downloadStartTime.get() == 0L) {
            this.downloadStartTime.set(System.currentTimeMillis());
        }
        this.downloadedBytes.set(downloaded);
        this.totalBytes.set(total);
    }
    
    @Nullable
    public DownloadProgress getDownloadProgress() {
        if (!this.downloadLock.get()) {
            return null;
        }
        final long start = this.downloadStartTime.get();
        final long downloaded = this.downloadedBytes.get();
        final long total = this.totalBytes.get();
        if (start == 0L || total <= 0L) {
            return new DownloadProgress(0, 0L, total, -1L);
        }
        final int percent = (int)(downloaded * 100L / total);
        final long elapsed = System.currentTimeMillis() - start;
        long etaSeconds = -1L;
        if (elapsed > 0L && downloaded > 0L) {
            final double bytesPerMs = downloaded / (double)elapsed;
            final long remaining = total - downloaded;
            etaSeconds = (long)(remaining / bytesPerMs / 1000.0);
        }
        return new DownloadProgress(percent, downloaded, total, etaSeconds);
    }
    
    public boolean cancelDownload() {
        final CompletableFuture<?> download = this.activeDownload.getAndSet(null);
        final Thread thread = this.activeDownloadThread.getAndSet(null);
        if (thread != null) {
            thread.interrupt();
        }
        if (download == null && thread == null) {
            return false;
        }
        if (download != null) {
            download.cancel(true);
        }
        this.releaseDownloadLock();
        return true;
    }
    
    private boolean shouldEnableUpdateChecker() {
        if (!ManifestUtil.isJar()) {
            UpdateModule.LOGGER.at(Level.INFO).log("Update checker disabled: not running from JAR");
            return false;
        }
        if (Constants.SINGLEPLAYER) {
            UpdateModule.LOGGER.at(Level.INFO).log("Update checker disabled: singleplayer mode");
            return false;
        }
        final HytaleServerConfig.UpdateConfig config = HytaleServer.get().getConfig().getUpdateConfig();
        if (!config.isEnabled()) {
            UpdateModule.LOGGER.at(Level.INFO).log("Update checker disabled: disabled in config");
            return false;
        }
        final String manifestPatchline = ManifestUtil.getPatchline();
        final String configPatchline = config.getPatchline();
        if ("dev".equals(manifestPatchline) && (configPatchline == null || configPatchline.isEmpty())) {
            UpdateModule.LOGGER.at(Level.INFO).log("Update checker disabled: dev patchline (set Patchline in config to override)");
            return false;
        }
        if (!UpdateService.isValidUpdateLayout()) {
            UpdateModule.LOGGER.at(Level.WARNING).log("Update checker disabled: invalid folder layout. Expected to run from Server/ with Assets.zip and start.sh/bat in parent directory.");
            return false;
        }
        return true;
    }
    
    private void performUpdateCheck() {
        final ServerAuthManager authManager = ServerAuthManager.getInstance();
        if (!authManager.hasSessionToken()) {
            UpdateModule.LOGGER.at(Level.FINE).log("Not authenticated - skipping update check");
            return;
        }
        final String stagedVersion = UpdateService.getStagedVersion();
        if (stagedVersion != null) {
            UpdateModule.LOGGER.at(Level.FINE).log("Staged update already exists (%s) - skipping update check", stagedVersion);
            this.startAutoApplyTaskIfNeeded();
            return;
        }
        if (this.isDownloadInProgress()) {
            UpdateModule.LOGGER.at(Level.FINE).log("Download in progress - skipping update check");
            return;
        }
        final UpdateService updateService = new UpdateService();
        final String patchline = UpdateService.getEffectivePatchline();
        updateService.checkForUpdate(patchline).thenAccept(manifest -> {
            if (manifest == null) {
                UpdateModule.LOGGER.at(Level.FINE).log("Update check returned no result");
            }
            else {
                this.setLatestKnownVersion(manifest);
                final String currentVersion = ManifestUtil.getImplementationVersion();
                if (currentVersion != null && currentVersion.equals(manifest.version)) {
                    UpdateModule.LOGGER.at(Level.FINE).log("Already running latest version: %s", currentVersion);
                }
                else {
                    this.logUpdateAvailable(currentVersion, manifest.version);
                    final HytaleServerConfig.UpdateConfig config = HytaleServer.get().getConfig().getUpdateConfig();
                    if (config.isNotifyPlayersOnAvailable()) {
                        this.notifyPlayers(manifest.version);
                    }
                    if (config.getAutoApplyMode() != HytaleServerConfig.UpdateConfig.AutoApplyMode.DISABLED) {
                        UpdateModule.LOGGER.at(Level.INFO).log("Auto-downloading update %s...", manifest.version);
                        this.autoDownloadUpdate(updateService, manifest);
                    }
                }
            }
        });
    }
    
    private void autoDownloadUpdate(@Nonnull final UpdateService updateService, @Nonnull final UpdateService.VersionManifest manifest) {
        if (UpdateService.getStagedVersion() != null || !this.tryAcquireDownloadLock()) {
            return;
        }
        final UpdateService.DownloadTask downloadTask = updateService.downloadUpdate(manifest, UpdateService.getStagingDir(), (percent, downloaded, total) -> this.updateDownloadProgress(downloaded, total));
        final CompletableFuture<Boolean> downloadFuture = downloadTask.future().whenComplete((success, error) -> {
            this.releaseDownloadLock();
            if (Boolean.TRUE.equals(success)) {
                UpdateModule.LOGGER.at(Level.INFO).log("Update %s downloaded and staged", manifest.version);
                this.startAutoApplyTaskIfNeeded();
            }
            else if (error instanceof CancellationException) {
                UpdateModule.LOGGER.at(Level.INFO).log("Download of update %s was cancelled", manifest.version);
                UpdateService.deleteStagedUpdate();
            }
            else {
                UpdateModule.LOGGER.at(Level.WARNING).log("Failed to download update %s: %s", manifest.version, (error != null) ? error.getMessage() : "unknown error");
                UpdateService.deleteStagedUpdate();
            }
            return;
        });
        this.setActiveDownload(downloadFuture, downloadTask.thread());
    }
    
    private void performAutoApplyCheck() {
        final String stagedVersion = UpdateService.getStagedVersion();
        if (stagedVersion == null) {
            if (this.autoApplyScheduledTime.getAndSet(0L) != 0L) {
                UpdateModule.LOGGER.at(Level.FINE).log("No staged update - clearing auto-apply schedule");
            }
            this.lastWarningTime.set(0L);
            return;
        }
        UpdateModule.LOGGER.at(Level.FINE).log("Auto-apply check: staged version %s", stagedVersion);
        this.checkAutoApply(stagedVersion);
    }
    
    private void logUpdateAvailable(@Nullable final String currentVersion, @Nonnull final String latestVersion) {
        UpdateModule.LOGGER.at(Level.INFO).log("Update available: %s (current: %s)", latestVersion, currentVersion);
        final HytaleServerConfig.UpdateConfig config = HytaleServer.get().getConfig().getUpdateConfig();
        if (config.getAutoApplyMode() == HytaleServerConfig.UpdateConfig.AutoApplyMode.DISABLED) {
            UpdateModule.LOGGER.at(Level.INFO).log("Run '/update download' to stage the update");
        }
    }
    
    private void logStagedUpdateWarning(@Nullable final String version, final boolean isStartup) {
        final String border = "\u001b[0;33m===============================================================================================";
        UpdateModule.LOGGER.at(Level.INFO).log(border);
        if (isStartup) {
            UpdateModule.LOGGER.at(Level.INFO).log("%s         WARNING: Staged update %s not applied!", "\u001b[0;33m", (version != null) ? version : "unknown");
            UpdateModule.LOGGER.at(Level.INFO).log("%s         Use launcher script (start.sh/bat) or manually move files from updater/staging/", "\u001b[0;33m");
        }
        else {
            UpdateModule.LOGGER.at(Level.INFO).log("%s         REMINDER: Staged update %s waiting to be applied", "\u001b[0;33m", (version != null) ? version : "unknown");
            UpdateModule.LOGGER.at(Level.INFO).log("%s         Run '/update status' for details or '/update cancel' to abort", "\u001b[0;33m");
        }
        UpdateModule.LOGGER.at(Level.INFO).log(border);
    }
    
    private void checkAutoApply(@Nonnull final String stagedVersion) {
        final HytaleServerConfig.UpdateConfig config = HytaleServer.get().getConfig().getUpdateConfig();
        final HytaleServerConfig.UpdateConfig.AutoApplyMode mode = config.getAutoApplyMode();
        if (mode == HytaleServerConfig.UpdateConfig.AutoApplyMode.DISABLED) {
            return;
        }
        final Universe universe = Universe.get();
        if (universe == null) {
            return;
        }
        final int playerCount = universe.getPlayers().size();
        if (playerCount == 0) {
            UpdateModule.LOGGER.at(Level.INFO).log("No players online - auto-applying update %s", stagedVersion);
            this.triggerAutoApply();
            return;
        }
        if (mode == HytaleServerConfig.UpdateConfig.AutoApplyMode.WHEN_EMPTY) {
            return;
        }
        final int delayMinutes = config.getAutoApplyDelayMinutes();
        final long now = System.currentTimeMillis();
        final long applyTime = now + delayMinutes * 60 * 1000L;
        if (this.autoApplyScheduledTime.compareAndSet(0L, applyTime)) {
            UpdateModule.LOGGER.at(Level.INFO).log("Update %s will be auto-applied in %d minutes (players online: %d)", stagedVersion, delayMinutes, playerCount);
            this.broadcastToPlayers(Message.translation("server.update.auto_apply_warning").param("version", stagedVersion).param("minutes", delayMinutes).color(Color.YELLOW));
            return;
        }
        final long scheduledTime = this.autoApplyScheduledTime.get();
        if (now >= scheduledTime - 2000L) {
            UpdateModule.LOGGER.at(Level.INFO).log("Auto-apply delay expired - applying update %s", stagedVersion);
            this.broadcastToPlayers(Message.translation("server.update.auto_apply_now").param("version", stagedVersion).color(Color.RED));
            this.triggerAutoApply();
            return;
        }
        final long remainingMinutes = (scheduledTime - now) / 60000L;
        final long warnInterval = (remainingMinutes <= 1L) ? 30000L : 300000L;
        final long lastWarn = this.lastWarningTime.get();
        if (now - lastWarn >= warnInterval && this.lastWarningTime.compareAndSet(lastWarn, now)) {
            UpdateModule.LOGGER.at(Level.INFO).log("Update %s will be auto-applied in %d minute(s)", stagedVersion, Math.max(1L, remainingMinutes));
            this.broadcastToPlayers(Message.translation("server.update.auto_apply_warning").param("version", stagedVersion).param("minutes", Math.max(1L, remainingMinutes)).color(Color.YELLOW));
        }
    }
    
    private void triggerAutoApply() {
        this.autoApplyScheduledTime.set(0L);
        HytaleServer.get().shutdownServer(ShutdownReason.UPDATE);
    }
    
    private void broadcastToPlayers(@Nonnull final Message message) {
        final Universe universe = Universe.get();
        if (universe == null) {
            return;
        }
        for (final PlayerRef player : universe.getPlayers()) {
            player.sendMessage(message);
        }
    }
    
    private void notifyPlayers(@Nonnull final String version) {
        final Universe universe = Universe.get();
        if (universe == null) {
            return;
        }
        final Message message = Message.translation("server.update.notify_players").param("version", version);
        final PermissionsModule permissionsModule = PermissionsModule.get();
        for (final PlayerRef player : universe.getPlayers()) {
            if (permissionsModule.hasPermission(player.getUuid(), "hytale.system.update.notify")) {
                player.sendMessage(message);
            }
        }
    }
    
    static {
        MANIFEST = PluginManifest.corePlugin(UpdateModule.class).build();
        LOGGER = HytaleLogger.forEnclosingClass();
        KILL_SWITCH_ENABLED = SystemUtil.getEnvBoolean("HYTALE_DISABLE_UPDATES");
    }
    
    record DownloadProgress(int percent, long downloadedBytes, long totalBytes, long etaSeconds) {}
}
