package io.xpipe.app.core;

import io.xpipe.app.core.check.AppDirectoryPermissionsCheck;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.beacon.BeaconServer;
import io.xpipe.app.util.OsType;
import io.xpipe.app.util.XPipeDaemonMode;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.Value;
import lombok.experimental.NonFinal;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Instant;
import java.util.*;

@Value
public class AppProperties {

    private static AppProperties INSTANCE;

    boolean fullVersion;
    String version;
    String build;
    UUID buildUuid;
    String sentryUrl;
    String arch;
    boolean image;
    boolean staging;
    boolean useVirtualThreads;
    boolean debugThreads;
    Path dataDir;
    Path defaultReleaseDataDir;
    Path defaultDataDir;
    Path dataBinDir;
    boolean showcase;
    AppVersion canonicalVersion;
    boolean locatePtb;
    boolean locatorVersionCheck;
    boolean isTest;
    boolean autoAcceptEula;
    UUID uuid;
    boolean initialLaunch;
    boolean restarted;
    UUID sessionId;
    boolean developerMode;
    boolean newBuildSession;
    boolean aotTrainMode;
    boolean debugPlatformThreadAccess;
    boolean persistData;
    AppArguments arguments;
    XPipeDaemonMode explicitMode;
    String devLoginPassword;
    boolean logToSysOut;
    boolean logToFile;
    boolean logPlatformDebug;
    String logLevel;
    String loginTarget;
    int defaultBeaconPort;
    Path beaconAuthFile;
    Path beaconLockFile;
    boolean debugCli;
    boolean isDaemon;
    boolean isCli;
    @NonFinal
    @Getter(AccessLevel.PRIVATE)
    Integer effectiveBeaconPort;

    public AppProperties(String[] args) {
        var appDir = Path.of(System.getProperty("user.dir")).resolve("app");
        Path propsFile = appDir.resolve("dev.properties");
        if (Files.exists(propsFile)) {
            try {
                Properties props = new Properties();
                props.load(Files.newInputStream(propsFile));
                props.forEach((key, value) -> {
                    // Don't overwrite existing properties
                    if (System.getProperty(key.toString()) == null) {
                        System.setProperty(key.toString(), value.toString());
                    }
                });
            } catch (IOException e) {
                ErrorEventFactory.fromThrowable(e).handle();
            }
        }
        var referenceDir = Files.exists(appDir) ? appDir : Path.of(System.getProperty("user.dir"));

        image = AppProperties.class
                .getProtectionDomain()
                .getCodeSource()
                .getLocation()
                .getProtocol()
                .equals("jrt");
        arguments = AppArguments.init(args);
        fullVersion = Optional.ofNullable(System.getProperty(AppNames.propertyName("fullVersion")))
                .map(Boolean::parseBoolean)
                .orElse(false);
        version = Optional.ofNullable(System.getProperty(AppNames.propertyName("version")))
                .orElse("dev");
        build = Optional.ofNullable(System.getProperty(AppNames.propertyName("build")))
                .orElse("unknown");
        buildUuid = Optional.ofNullable(System.getProperty(AppNames.propertyName("buildId")))
                .map(UUID::fromString)
                .orElse(UUID.randomUUID());
        sentryUrl = System.getProperty(AppNames.propertyName("sentryUrl"));
        arch = System.getProperty("os.arch").equals("amd64") ? "x86_64" : "arm64";
        staging = Optional.ofNullable(System.getProperty(AppNames.propertyName("staging")))
                .map(Boolean::parseBoolean)
                .orElse(false);
        devLoginPassword = System.getProperty(AppNames.propertyName("loginPassword"));
        useVirtualThreads = Optional.ofNullable(System.getProperty(AppNames.propertyName("useVirtualThreads")))
                .map(Boolean::parseBoolean)
                .orElse(true);
        debugThreads = Optional.ofNullable(System.getProperty(AppNames.propertyName("debugThreads")))
                .map(Boolean::parseBoolean)
                .orElse(false);
        debugPlatformThreadAccess = Optional.ofNullable(
                        System.getProperty(AppNames.propertyName("debugPlatformThreadAccess")))
                .map(Boolean::parseBoolean)
                .orElse(false);
        defaultReleaseDataDir = AppSystemInfo.ofCurrent().getUserHome().resolve(".xpipe");
        defaultDataDir = AppSystemInfo.ofCurrent().getUserHome().resolve(isStaging() ? ".xpipe-ptb" : ".xpipe");
        dataDir = Optional.ofNullable(System.getProperty(AppNames.propertyName("dataDir")))
                .map(s -> {
                    var p = Path.of(s);
                    if (!p.isAbsolute()) {
                        p = referenceDir.resolve(p);
                    }
                    return p;
                })
                .orElse(defaultDataDir);
        showcase = Optional.ofNullable(System.getProperty(AppNames.propertyName("showcase")))
                .map(Boolean::parseBoolean)
                .orElse(false);
        canonicalVersion = AppVersion.parse(version).orElse(null);
        locatePtb = Optional.ofNullable(System.getProperty(AppNames.propertyName("locatorUsePtbInstallation")))
                .map(Boolean::parseBoolean)
                .orElse(false);
        locatorVersionCheck = Optional.ofNullable(
                        System.getProperty(AppNames.propertyName("locatorDisableInstallationVersionCheck")))
                .map(s -> !Boolean.parseBoolean(s))
                .orElse(true);
        isTest = isJUnitTest();
        autoAcceptEula = Optional.ofNullable(System.getProperty(AppNames.propertyName("acceptEula")))
                .map(Boolean::parseBoolean)
                .orElse(false);
        restarted = Optional.ofNullable(System.getProperty(AppNames.propertyName("restarted")))
                .map(Boolean::parseBoolean)
                .orElse(false);
        developerMode = Optional.ofNullable(System.getProperty(AppNames.propertyName("developerMode")))
                .map(Boolean::parseBoolean)
                .orElse(false);
        persistData = Optional.ofNullable(System.getProperty(AppNames.propertyName("persistData")))
                .map(Boolean::parseBoolean)
                .orElse(true);
        logToSysOut = Optional.ofNullable(System.getProperty(AppNames.propertyName("writeSysOut")))
                .map(Boolean::parseBoolean)
                .orElse(false);
        logToFile = Optional.ofNullable(System.getProperty(AppNames.propertyName("writeLogs")))
                .map(Boolean::parseBoolean)
                .orElse(true);
        logPlatformDebug = Optional.ofNullable(System.getProperty(AppNames.propertyName("debugPlatform")))
                .map(Boolean::parseBoolean)
                .orElse(false);
        logLevel = Optional.ofNullable(System.getProperty(AppNames.propertyName("logLevel")))
                .filter(s -> AppLogs.LOG_LEVELS.contains(s))
                .orElse("info");
        loginTarget = Optional.ofNullable(System.getProperty(AppNames.propertyName("login")))
                .orElse(null);
        isCli = Optional.ofNullable(System.getProperty(AppNames.propertyName("isCli")))
                .map(Boolean::parseBoolean)
                .orElse(false);
        isDaemon = Optional.ofNullable(System.getProperty(AppNames.propertyName("isDaemon")))
                .map(Boolean::parseBoolean)
                .orElse(!isCli);

        // We require the user dir from here
        AppDirectoryPermissionsCheck.checkDirectory(dataDir);
        AppCache.setBasePath(dataDir.resolve("cache"));
        dataBinDir = dataDir.resolve("cache", "bin");
        UUID id = AppCache.getNonNull("uuid", UUID.class, () -> null);
        if (id == null) {
            uuid = UUID.randomUUID();
            AppCache.update("uuid", uuid);
        } else {
            uuid = id;
        }
        initialLaunch = AppCache.getNonNull("lastBuildId", String.class, () -> null) == null;
        sessionId = UUID.randomUUID();
        var cachedBuildId = AppCache.getNonNull("lastBuildId", String.class, () -> null);
        newBuildSession = !buildUuid.toString().equals(cachedBuildId);
        AppCache.update("lastBuildId", buildUuid);
        aotTrainMode = Optional.ofNullable(System.getProperty(AppNames.propertyName("aotTrainMode")))
                .map(Boolean::parseBoolean)
                .orElse(false);
        explicitMode = XPipeDaemonMode.getIfPresent(System.getProperty(AppNames.propertyName("mode")))
                .orElse(null);
        defaultBeaconPort = getDefaultBeaconPort(staging);
        beaconAuthFile = getLocalBeaconAuthFile(staging);
        beaconLockFile = beaconAuthFile.getParent().resolve("lock");
        clearLeftoverAuthFile();
        debugCli = Optional.ofNullable(System.getProperty(AppNames.propertyName("debugCli")))
                .map(Boolean::parseBoolean)
                .orElse(false);
    }

    public OptionalInt queryEffectiveBeaconPort(boolean reachable) {
        if (effectiveBeaconPort != null) {
            return OptionalInt.of(effectiveBeaconPort);
        }

        if (!reachable) {
            effectiveBeaconPort = getDefaultBeaconPort();
            return OptionalInt.of(effectiveBeaconPort);
        }

        var authFile = getBeaconAuthFile();
        if (Files.exists(authFile)) {
            effectiveBeaconPort = getDefaultBeaconPort();
            return OptionalInt.of(effectiveBeaconPort);
        }

        var hasEnv = System.getenv("BEACON_PORT") != null || System.getenv("XPIPE_BEACON_PORT") != null;
        if (hasEnv) {
            effectiveBeaconPort = getDefaultBeaconPort();
            return OptionalInt.of(effectiveBeaconPort);
        }

        var start = 21723;
        for (int i = 0; i < 20; i++) {
            var p = start + i;
            var occupied = BeaconServer.isReachable(p);
            if (!occupied) {
                effectiveBeaconPort = p;
                return OptionalInt.of(p);
            }
        }
        return OptionalInt.empty();
    }

    private void clearLeftoverAuthFile() {
        var hasAuthFile = Files.exists(beaconAuthFile);
        if (hasAuthFile) {
            try (var channel = new RandomAccessFile(beaconLockFile.toFile(), "rw").getChannel()) {
                var lock = channel.tryLock();
                if (lock != null) {
                    lock.release();
                    Files.delete(beaconAuthFile);
                }
            } catch (Exception ignored) {
            }
        }
    }

    private static int getDefaultBeaconPort(boolean staging) {
        var customPortVar = System.getenv("XPIPE_BEACON_PORT");
        Integer customPort = null;
        if (customPortVar != null) {
            try {
                customPort = Integer.parseInt(customPortVar);
            } catch (NumberFormatException ignored) {
            }
        }

        var rawPortProp = System.getProperty(AppNames.propertyName("beaconPort"));
        if (customPort == null && rawPortProp != null) {
            try {
                var parsed = Integer.parseInt(rawPortProp);
                customPort = parsed;
            } catch (NumberFormatException ignored) {
            }
        }

        var effectivePort = customPort != null ? customPort : 21721 + (staging ? 1 : 0);;
        return effectivePort;
    }

    private static Path getLocalBeaconAuthFile(boolean staging) {
        if (OsType.ofLocal() == OsType.LINUX) {
            var name = AppSystemInfo.ofCurrent().getUser();
            return AppSystemInfo.ofCurrent().getTemp().resolve(staging ? "xpipe-ptb" : "xpipe", name, "beacon-auth");
        } else {
            var path = AppSystemInfo.ofCurrent().getTemp().resolve(staging ? "xpipe-ptb" : "xpipe", "beacon-auth");
            return path;
        }
    }

    private static boolean isJUnitTest() {
        for (StackTraceElement element : Thread.currentThread().getStackTrace()) {
            if (element.getClassName().startsWith("org.junit.")) {
                return true;
            }
        }
        return false;
    }

    public static void init() {
        init(new String[0]);
    }

    public static void init(String[] args) {
        if (INSTANCE != null) {
            return;
        }

        INSTANCE = new AppProperties(args);
    }

    public static AppProperties get() {
        return INSTANCE;
    }

    public void resetInitialLaunch() {
        AppCache.clear("lastBuildId");
    }

    public void logArguments() {
        TrackEvent.withInfo("Loaded properties")
                .tag("version", version)
                .tag("build", build)
                .tag("dataDir", dataDir)
                .tag("fullVersion", fullVersion)
                .handle();

        TrackEvent.withInfo("Received arguments")
                .tag("raw", arguments.getRawArgs())
                .tag("resolved", arguments.getResolvedArgs())
                .tag("resolvedCommand", arguments.getDaemonOpenArgs())
                .handle();

        for (var e : System.getProperties().entrySet()) {
            if (e.getKey().toString().contains(AppNames.ofCurrent().getGroupName())) {
                TrackEvent.debug("Detected app property " + e.getKey() + "=" + e.getValue());
            }
        }
    }

    public boolean isDevelopmentEnvironment() {
        return !isImage() && isDeveloperMode();
    }

    public Optional<AppVersion> getCanonicalVersion() {
        return Optional.ofNullable(canonicalVersion);
    }

    public Instant getFirstStartupDate() {
        var dir = defaultDataDir;
        if (!Files.exists(dir)) {
            return Instant.now();
        }

        try {
            var attr = Files.readAttributes(dir, BasicFileAttributes.class);
            return attr.creationTime().toInstant();
        } catch (Exception e) {
            ErrorEventFactory.fromThrowable(e).expected().omit().handle();
            return Instant.now();
        }
    }
}
