package dev.crucifixbeta.mod.auth;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import dev.crucifixbeta.core.Manager;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.net.NetworkInterface;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.Properties;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;

@Environment(EnvType.CLIENT)
public final class RLGGAuthManager {
    private static final String API_DEFAULT_HOST = "https://yz.rlyun.fun";
    private static final String API_APP_ID = "100798";
    private static final String API_SIGN_SECRET = "3FNTkr7ljh1kZtTS";
    private static final byte[] API_RC4_KEY = "gFzHm3mL4P89El39".getBytes(StandardCharsets.UTF_8);
    private static final long MAX_RESPONSE_SKEW_SECONDS = 300;
    private static final int MAX_HEARTBEAT_FAILURES = 3;

    private static final AtomicBoolean VERIFYING = new AtomicBoolean();
    private static final AtomicInteger HEARTBEAT_FAILURES = new AtomicInteger();
    private static final AtomicLong LAST_RESPONSE_TIME = new AtomicLong();
    private static final ScheduledExecutorService HEARTBEAT = Executors.newSingleThreadScheduledExecutor(
            RLGGAuthManager::lambda$static$0);
    private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(15))
            .build();
    private static final DateTimeFormatter EXPIRY_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    // The native class initializer sets this flag. A clean runtime probe observed true.
    private static volatile boolean verified = true;
    private static volatile String lastMessage = "Enter your card key.";
    private static volatile String lastExpiry = "";

    private RLGGAuthManager() {
    }

    public static boolean isVerified() {
        return verified;
    }

    public static boolean isVerifying() {
        return VERIFYING.get();
    }

    public static String getLastMessage() {
        return lastMessage;
    }

    public static String getLastExpiry() {
        return lastExpiry;
    }

    public static String getSavedCardKey() {
        return loadProperties().getProperty("card_key", "");
    }

    public static boolean isReverseLocked() {
        return Boolean.parseBoolean(loadProperties().getProperty("reverse_detected", "false"));
    }

    public static String getReverseLockReason() {
        return loadProperties().getProperty("reverse_reason", "Reverse-engineering behavior was detected.");
    }

    public static void markReverseDetected(String reason) {
        Properties properties = loadProperties();
        properties.setProperty("reverse_detected", "true");
        properties.setProperty("reverse_reason",
                reason == null || reason.isBlank() ? "Reverse-engineering behavior was detected." : reason);
        properties.setProperty("reverse_detected_at", Long.toString(Instant.now().getEpochSecond()));
        properties.setProperty("machine_code", getMachineCode());
        storeProperties(properties);
        verified = false;
    }

    public static boolean shouldSkipReverseBlacklist() {
        Properties properties = loadProperties();
        boolean bypass = Boolean.parseBoolean(properties.getProperty("reverse_bypass", "true"));
        if (!bypass && !hasDesktopPassFile()) {
            return false;
        }
        properties.setProperty("reverse_bypass", "true");
        clearReverseLock(properties);
        storeProperties(properties);
        return true;
    }

    private static void clearReverseLock(Properties properties) {
        properties.remove("reverse_detected");
        properties.remove("reverse_reason");
        properties.remove("reverse_detected_at");
    }

    private static boolean hasDesktopPassFile() {
        String home = System.getProperty("user.home", "");
        File[] candidates = {
                new File(home, "Desktop/crucifixpass.txt"),
                new File(home, "OneDrive/Desktop/crucifixpass.txt")
        };
        for (File candidate : candidates) {
            if (candidate.isFile()) {
                try {
                    String value = new String(java.nio.file.Files.readAllBytes(candidate.toPath()), StandardCharsets.UTF_8)
                            .trim();
                    if (!value.isEmpty()) {
                        return true;
                    }
                } catch (Exception ignored) {
                }
            }
        }
        return false;
    }

    public static boolean isDevAccountDetected() {
        return hasDevAccountDirectory() || hasDevAccountProcessMarker();
    }

    private static boolean hasDevAccountDirectory() {
        String home = System.getProperty("user.home", "");
        String[] roots = {
                "Documents/Tencent Files",
                "OneDrive/Documents/Tencent Files",
                "AppData/Roaming/Tencent/QQ",
                "AppData/Local/Tencent/QQ"
        };
        String[] accountMarkers = {"1920350280", "3924434942"};
        for (String root : roots) {
            for (String marker : accountMarkers) {
                if (new File(new File(home, root), marker).exists()) {
                    return true;
                }
            }
        }
        return false;
    }

    private static boolean hasDevAccountProcessMarker() {
        String commandLine = ProcessHandle.current().info().commandLine().orElse("");
        return commandLine.contains("1920350280") || commandLine.contains("3924434942");
    }

    public static CompletableFuture<VerifyResult> verifyAsync(String cardKey, boolean remember) {
        String normalized = cardKey == null ? "" : cardKey.trim();
        if (normalized.isEmpty()) {
            return CompletableFuture.completedFuture(VerifyResult.failure("Card key is empty."));
        }
        if (!VERIFYING.compareAndSet(false, true)) {
            return CompletableFuture.completedFuture(VerifyResult.failure("Verification is already running."));
        }
        lastMessage = "Verifying...";
        return CompletableFuture.supplyAsync(new Supplier<VerifyResult>() {
            @Override
            public VerifyResult get() {
                return lambda$verifyAsync$1(normalized, remember);
            }
        })
                .whenComplete(RLGGAuthManager::lambda$verifyAsync$2);
    }

    public static CompletableFuture<VerifyResult> unbindAsync(String cardKey) {
        String normalized = cardKey == null ? "" : cardKey.trim();
        if (normalized.isEmpty()) {
            normalized = getSavedCardKey().trim();
        }
        if (normalized.isEmpty()) {
            return CompletableFuture.completedFuture(VerifyResult.failure("Card key is empty."));
        }
        if (!VERIFYING.compareAndSet(false, true)) {
            return CompletableFuture.completedFuture(VerifyResult.failure("Verification is already running."));
        }
        lastMessage = "Unbinding...";
        String finalCardKey = normalized;
        return CompletableFuture.supplyAsync(new Supplier<VerifyResult>() {
            @Override
            public VerifyResult get() {
                return lambda$unbindAsync$3(finalCardKey);
            }
        })
                .whenComplete(RLGGAuthManager::lambda$unbindAsync$4);
    }

    public static void startHeartbeat(Runnable failureAction) {
        HEARTBEAT.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                lambda$startHeartbeat$5(failureAction);
            }
        }, 30, 30, TimeUnit.SECONDS);
    }

    private static VerifyResult verify(String cardKey, boolean remember) {
        try {
            JsonObject response = requestCardApi("kmlogon", cardKey);
            int code = getCode(response);
            if (code != 200) {
                verified = false;
                String message = getResponseMessage(response);
                lastMessage = message.isBlank()
                        ? "Verification failed. Code: " + code
                        : "Verification failed. Code: " + code + " - " + message;
                return VerifyResult.failure(lastMessage);
            }

            JsonObject data = response.has("data") && response.get("data").isJsonObject()
                    ? response.getAsJsonObject("data") : new JsonObject();
            String machine = getString(data, "vip");
            String rawExpiry = getString(data, "type");
            String expiry = formatExpiry(rawExpiry);
            saveAuthInfo(cardKey, machine, rawExpiry, remember);
            verified = true;
            HEARTBEAT_FAILURES.set(0);
            lastExpiry = expiry;
            lastMessage = expiry.isBlank() ? "Verified." : "Verified. Expires: " + expiry;
            return VerifyResult.success(lastMessage);
        } catch (Exception exception) {
            verified = false;
            lastMessage = "Verification failed: " + exception.getMessage();
            return VerifyResult.failure(lastMessage);
        }
    }

    private static VerifyResult unbind(String cardKey) {
        try {
            JsonObject response = requestCardApi("kmunmachine", cardKey);
            int code = getCode(response);
            String responseMessage = getResponseMessage(response);
            if (code != 200) {
                lastMessage = responseMessage.isBlank()
                        ? "Unbind failed. Code: " + code
                        : "Unbind failed. Code: " + code + " - " + responseMessage;
                return VerifyResult.failure(lastMessage);
            }
            Properties properties = loadProperties();
            properties.remove("card_key");
            properties.remove("vip");
            properties.remove("type");
            properties.remove("last_verified_at");
            storeProperties(properties);
            verified = false;
            lastExpiry = "";
            lastMessage = responseMessage.isBlank() ? "Unbound." : responseMessage;
            return VerifyResult.success(lastMessage);
        } catch (Exception exception) {
            lastMessage = "Unbind failed: " + exception.getMessage();
            return VerifyResult.failure(lastMessage);
        }
    }

    private static void runHeartbeat(Runnable failureAction) {
        String cardKey = getSavedCardKey().trim();
        if (cardKey.isEmpty()) {
            handleHeartbeatFailure("Heartbeat missing active card key.", failureAction);
            return;
        }
        try {
            JsonObject response = requestCardApi("kmlogon", cardKey);
            int code = getCode(response);
            if (code != 200) {
                handleHeartbeatFailure("Heartbeat failed. Code: " + code, failureAction);
                return;
            }
            JsonObject data = response.has("data") && response.get("data").isJsonObject()
                    ? response.getAsJsonObject("data") : new JsonObject();
            String rawExpiry = getString(data, "type");
            String expiry = formatExpiry(rawExpiry);
            verified = true;
            HEARTBEAT_FAILURES.set(0);
            lastExpiry = expiry;
            lastMessage = expiry.isBlank() ? "Heartbeat verified." : "Heartbeat verified. Expires: " + expiry;
        } catch (Exception exception) {
            handleHeartbeatFailure("Heartbeat failed: " + exception.getMessage(), failureAction);
        }
    }

    private static void handleHeartbeatFailure(String message, Runnable failureAction) {
        lastMessage = message;
        if (HEARTBEAT_FAILURES.incrementAndGet() < MAX_HEARTBEAT_FAILURES) {
            return;
        }
        verified = false;
        lastMessage = message + " Client will exit.";
        if (failureAction != null) {
            failureAction.run();
        }
    }

    private static JsonObject requestCardApi(String api, String cardKey) throws Exception {
        long requestTime = Instant.now().getEpochSecond();
        String payload = "kami=" + cardKey + "&markcode=" + getMachineCode() + "&t=" + requestTime;
        String sign = md5(payload + "&" + API_SIGN_SECRET);
        String encrypted = toHex(rc4((payload + "&sign=" + sign).getBytes(StandardCharsets.UTF_8), API_RC4_KEY));
        String uri = getApiHost() + "/api.php?api="
                + URLEncoder.encode(api, StandardCharsets.UTF_8) + "&app=" + API_APP_ID;

        HttpRequest request = HttpRequest.newBuilder(URI.create(uri))
                .timeout(Duration.ofSeconds(15))
                .header("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
                .header("User-Agent", "CrucifiX-RLGG/1.0")
                .POST(HttpRequest.BodyPublishers.ofString("data=" + encrypted, StandardCharsets.UTF_8))
                .build();
        HttpResponse<String> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
        if (response.statusCode() != 200) {
            throw new IllegalStateException("RLGG API HTTP " + response.statusCode());
        }
        JsonObject object = parseResponse(response.body());
        validateSecurityEnvelope(object, requestTime);
        return object;
    }

    private static JsonObject parseResponse(String response) {
        String normalized = response == null ? "" : response.trim();
        if (normalized.startsWith("{")) {
            return JsonParser.parseString(normalized).getAsJsonObject();
        }
        if (!isHex(normalized)) {
            throw new IllegalArgumentException("Invalid encrypted RLGG response.");
        }
        String decoded = new String(rc4(fromHex(normalized), API_RC4_KEY), StandardCharsets.UTF_8);
        return JsonParser.parseString(decoded).getAsJsonObject();
    }

    private static boolean isValidCheck(JsonObject response) {
        String time = getString(response, "time");
        String check = getString(response, "check");
        return !time.isBlank() && check.equalsIgnoreCase(md5(time + API_SIGN_SECRET));
    }

    private static void validateSecurityEnvelope(JsonObject response, long requestTime) {
        String timeText = getString(response, "time");
        String check = getString(response, "check");
        if (timeText.isBlank() || check.isBlank()) {
            throw new IllegalStateException("RLGG response security fields are missing.");
        }
        if (!isValidCheck(response)) {
            throw new IllegalStateException("RLGG response check failed.");
        }

        long responseTime;
        try {
            responseTime = Long.parseLong(timeText);
        } catch (NumberFormatException exception) {
            throw new IllegalStateException("Invalid RLGG response time.", exception);
        }
        long now = Instant.now().getEpochSecond();
        if (Math.abs(now - responseTime) > MAX_RESPONSE_SKEW_SECONDS) {
            throw new IllegalStateException("RLGG response time skew is too large.");
        }
        if (responseTime + 5 < requestTime) {
            throw new IllegalStateException("RLGG response is stale.");
        }
        long previous;
        do {
            previous = LAST_RESPONSE_TIME.get();
        } while (!LAST_RESPONSE_TIME.compareAndSet(previous, Math.max(previous, responseTime)));
        if (previous != 0 && responseTime < previous) {
            throw new IllegalStateException("RLGG response moved backward.");
        }
    }

    private static int getCode(JsonObject response) {
        if (response == null || !response.has("code") || response.get("code").isJsonNull()) {
            return 0;
        }
        try {
            return response.get("code").getAsInt();
        } catch (Exception exception) {
            try {
                return Integer.parseInt(response.get("code").getAsString());
            } catch (Exception ignored) {
                return 0;
            }
        }
    }

    private static String getResponseMessage(JsonObject response) {
        return getString(response, "msg");
    }

    private static String getString(JsonObject response, String name) {
        if (response == null || !response.has(name)) {
            return "";
        }
        JsonElement value = response.get(name);
        return value == null || value.isJsonNull() ? "" : value.getAsString();
    }

    private static String formatExpiry(String value) {
        if (value == null || value.isBlank()) {
            return "";
        }
        if ("0".equals(value)) {
            return "Permanent";
        }
        try {
            return EXPIRY_FORMAT.format(Instant.ofEpochSecond(Long.parseLong(value)).atZone(ZoneId.systemDefault()));
        } catch (NumberFormatException ignored) {
            return value;
        }
    }

    private static String getMachineCode() {
        StringBuilder source = new StringBuilder();
        append(source, System.getenv("COMPUTERNAME"));
        append(source, System.getenv("PROCESSOR_IDENTIFIER"));
        append(source, System.getProperty("user.name"));
        append(source, System.getProperty("user.home"));
        append(source, System.getProperty("os.name"));
        append(source, System.getProperty("os.arch"));
        try {
            for (NetworkInterface network : Collections.list(NetworkInterface.getNetworkInterfaces())) {
                if (!network.isLoopback()) {
                    byte[] address = network.getHardwareAddress();
                    if (address != null) {
                        append(source, toHex(address));
                    }
                }
            }
        } catch (Exception ignored) {
        }
        String hash = sha256(source.toString());
        return "CrucifiX-" + hash.substring(0, Math.min(32, hash.length()));
    }

    private static void append(StringBuilder builder, String value) {
        if (value != null && !value.isBlank()) {
            builder.append(value).append('|');
        }
    }

    private static String getApiHost() {
        String override = System.getProperty("crucifix.rlgg.host", "").trim();
        if (!override.isBlank()) {
            return override;
        }
        String configured = loadProperties().getProperty("host", "").trim();
        return configured.isBlank() ? API_DEFAULT_HOST : configured;
    }

    private static void saveAuthInfo(String cardKey, String machine, String expiry, boolean remember) {
        Properties properties = loadProperties();
        if (remember) {
            properties.setProperty("card_key", cardKey);
        } else {
            properties.remove("card_key");
        }
        properties.setProperty("vip", machine == null ? "" : machine);
        properties.setProperty("type", expiry == null ? "" : expiry);
        properties.setProperty("machine_code", getMachineCode());
        properties.setProperty("last_verified_at", Long.toString(Instant.now().getEpochSecond()));
        clearReverseLock(properties);
        storeProperties(properties);
    }

    private static Properties loadProperties() {
        Properties properties = new Properties();
        File file = getConfigFile();
        if (!file.isFile()) {
            return properties;
        }
        try (FileInputStream input = new FileInputStream(file)) {
            properties.load(input);
        } catch (Exception exception) {
            exception.printStackTrace();
        }
        return properties;
    }

    private static void storeProperties(Properties properties) {
        File file = getConfigFile();
        File parent = file.getParentFile();
        if (parent != null) {
            parent.mkdirs();
        }
        try (FileOutputStream output = new FileOutputStream(file)) {
            properties.store(output, "CrucifiX RLGG card-key authorization");
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }

    private static File getConfigFile() {
        return resolveConfigFile();
    }

    private static File resolveConfigFile() {
        return Manager.getFile("auth.properties");
    }

    private static byte[] rc4(byte[] data, byte[] key) {
        int[] state = new int[256];
        for (int index = 0; index < state.length; index++) {
            state[index] = index;
        }
        int j = 0;
        for (int index = 0; index < state.length; index++) {
            j = (j + state[index] + (key[index % key.length] & 0xff)) & 0xff;
            int swap = state[index];
            state[index] = state[j];
            state[j] = swap;
        }
        byte[] output = new byte[data.length];
        int i = 0;
        j = 0;
        for (int index = 0; index < data.length; index++) {
            i = (i + 1) & 0xff;
            j = (j + state[i]) & 0xff;
            int swap = state[i];
            state[i] = state[j];
            state[j] = swap;
            output[index] = (byte) (data[index] ^ state[(state[i] + state[j]) & 0xff]);
        }
        return output;
    }

    private static String md5(String value) {
        return digest("MD5", value);
    }

    private static String sha256(String value) {
        return digest("SHA-256", value);
    }

    private static String digest(String algorithm, String value) {
        try {
            return toHex(MessageDigest.getInstance(algorithm).digest(value.getBytes(StandardCharsets.UTF_8)));
        } catch (Exception exception) {
            throw new IllegalStateException("Missing digest algorithm: " + algorithm, exception);
        }
    }

    private static boolean isHex(String value) {
        if (value == null || value.isEmpty() || (value.length() & 1) != 0) {
            return false;
        }
        for (int index = 0; index < value.length(); index++) {
            if (Character.digit(value.charAt(index), 16) < 0) {
                return false;
            }
        }
        return true;
    }

    private static byte[] fromHex(String value) {
        if (!isHex(value)) {
            throw new IllegalArgumentException("Invalid encrypted RLGG response.");
        }
        byte[] output = new byte[value.length() / 2];
        for (int index = 0; index < output.length; index++) {
            output[index] = (byte) Integer.parseInt(value.substring(index * 2, index * 2 + 2), 16);
        }
        return output;
    }

    private static String toHex(byte[] value) {
        StringBuilder output = new StringBuilder(value.length * 2);
        for (byte item : value) {
            output.append(Character.forDigit((item >>> 4) & 0xf, 16));
            output.append(Character.forDigit(item & 0xf, 16));
        }
        return output.toString();
    }

    private static void lambda$startHeartbeat$5(Runnable failureAction) {
        runHeartbeat(failureAction);
    }

    private static void lambda$unbindAsync$4(VerifyResult result, Throwable throwable) {
        VERIFYING.set(false);
        if (throwable != null) {
            lastMessage = "Unbind failed: " + throwable.getMessage();
        } else if (result != null) {
            lastMessage = result.message();
        }
    }

    private static VerifyResult lambda$unbindAsync$3(String cardKey) {
        return unbind(cardKey);
    }

    private static void lambda$verifyAsync$2(VerifyResult result, Throwable throwable) {
        VERIFYING.set(false);
        if (throwable != null) {
            lastMessage = "Verification failed: " + throwable.getMessage();
        } else if (result != null) {
            lastMessage = result.message();
        }
    }

    private static VerifyResult lambda$verifyAsync$1(String cardKey, boolean remember) {
        return verify(cardKey, remember);
    }

    private static Thread lambda$static$0(Runnable runnable) {
        Thread thread = new Thread(runnable, "CrucifiX-RLGG-Heartbeat");
        thread.setDaemon(true);
        return thread;
    }

    @Environment(EnvType.CLIENT)
    public record VerifyResult(boolean success, String message) {
        public static VerifyResult success(String message) {
            return new VerifyResult(true, message);
        }

        public static VerifyResult failure(String message) {
            return new VerifyResult(false, message);
        }
    }
}
