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

package com.hypixel.hytale.server.core.auth.oauth;

import java.io.IOException;
import com.sun.net.httpserver.HttpExchange;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.util.regex.Matcher;
import java.net.URLDecoder;
import java.util.regex.Pattern;
import java.security.MessageDigest;
import java.util.Base64;
import javax.annotation.Nullable;
import java.net.http.HttpResponse;
import com.hypixel.hytale.server.core.auth.AuthConfig;
import java.net.URI;
import java.net.http.HttpRequest;
import java.net.URLEncoder;
import java.io.OutputStream;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import com.hypixel.hytale.server.core.HytaleServer;
import java.util.logging.Level;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
import com.sun.net.httpserver.HttpServer;
import java.net.InetSocketAddress;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nonnull;
import java.time.Duration;
import java.net.http.HttpClient;
import java.security.SecureRandom;
import com.hypixel.hytale.logger.HytaleLogger;

public class OAuthClient
{
    private static final HytaleLogger LOGGER;
    private static final SecureRandom RANDOM;
    private final HttpClient httpClient;
    
    public OAuthClient() {
        this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10L)).build();
    }
    
    public Runnable startFlow(@Nonnull final OAuthBrowserFlow flow) {
        final AtomicBoolean cancelled = new AtomicBoolean(false);
        CompletableFuture.runAsync(() -> {
            HttpServer server = null;
            try {
                final String csrfState = this.generateRandomString(32);
                final String codeVerifier = this.generateRandomString(64);
                final String codeChallenge = this.generateCodeChallenge(codeVerifier);
                server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
                final int port = server.getAddress().getPort();
                final String encodedState = this.encodeStateWithPort(csrfState, port);
                final String redirectUri = "https://accounts.hytale.com/consent/client";
                final CompletableFuture<String> authCodeFuture = new CompletableFuture<String>();
                final HttpServer finalServer = server;
                final String expectedState = csrfState;
                server.createContext("/", exchange -> {
                    try {
                        final String query = exchange.getRequestURI().getQuery();
                        final String code = this.extractParam(query, "code");
                        final String returnedEncodedState = this.extractParam(query, "state");
                        String response;
                        int statusCode;
                        if (returnedEncodedState == null || !returnedEncodedState.equals(expectedState)) {
                            response = buildHtmlPage(false, "Authentication Failed", "Authentication Failed", "Something went wrong during authentication. Please close this window and try again.", "Invalid state parameter");
                            statusCode = 400;
                            authCodeFuture.completeExceptionally(new Exception("Invalid state"));
                        }
                        else if (code == null || code.isEmpty()) {
                            final String error = this.extractParam(query, "error");
                            final String errorMsg = (error != null) ? error : "No code received";
                            response = buildHtmlPage(false, "Authentication Failed", "Authentication Failed", "Something went wrong during authentication. Please close this window and try again.", errorMsg);
                            statusCode = 400;
                            authCodeFuture.completeExceptionally(new Exception(errorMsg));
                        }
                        else {
                            response = buildHtmlPage(true, "Authentication Successful", "Authentication Successful", "You have been logged in successfully. You can now close this window and return to the server.", null);
                            statusCode = 200;
                            authCodeFuture.complete(code);
                        }
                        exchange.sendResponseHeaders(statusCode, response.length());
                        try (final OutputStream os = exchange.getResponseBody()) {
                            os.write(response.getBytes(StandardCharsets.UTF_8));
                        }
                    }
                    catch (final Exception e2) {
                        OAuthClient.LOGGER.at(Level.WARNING).withCause(e2).log("Error handling OAuth callback");
                    }
                    finally {
                        HytaleServer.SCHEDULED_EXECUTOR.schedule(() -> finalServer.stop(0), 1L, TimeUnit.SECONDS);
                    }
                    return;
                });
                server.setExecutor(null);
                server.start();
                final String authUrl = this.buildAuthUrl(encodedState, codeChallenge, redirectUri);
                flow.onFlowInfo(authUrl);
                final String authCode = authCodeFuture.get(5L, TimeUnit.MINUTES);
                if (cancelled.get()) {
                    flow.onFailure("Authentication cancelled");
                }
                else {
                    final TokenResponse oauthTokens = this.exchangeCodeForTokens(authCode, codeVerifier, redirectUri);
                    if (oauthTokens == null) {
                        flow.onFailure("Failed to exchange authorization code for tokens");
                    }
                    else {
                        flow.onSuccess(oauthTokens);
                    }
                }
            }
            catch (final Exception e) {
                OAuthClient.LOGGER.at(Level.WARNING).withCause(e).log("OAuth browser flow failed");
                if (!cancelled.get()) {
                    flow.onFailure(e.getMessage());
                }
            }
            finally {
                if (server != null) {
                    server.stop(0);
                }
            }
            return;
        });
        return () -> cancelled.set(true);
    }
    
    public Runnable startFlow(final OAuthDeviceFlow flow) {
        final AtomicBoolean cancelled = new AtomicBoolean(false);
        CompletableFuture.runAsync(() -> {
            try {
                final DeviceAuthResponse deviceAuth = this.requestDeviceAuthorization();
                if (deviceAuth == null) {
                    flow.onFailure("Failed to start device authorization");
                }
                else {
                    flow.onFlowInfo(deviceAuth.userCode(), deviceAuth.verificationUri(), deviceAuth.verificationUriComplete(), deviceAuth.expiresIn());
                    int pollInterval = Math.max(deviceAuth.interval, 15);
                    final long deadline = System.currentTimeMillis() + deviceAuth.expiresIn * 1000L;
                    while (System.currentTimeMillis() < deadline && !cancelled.get()) {
                        Thread.sleep(pollInterval * 1000L);
                        final TokenResponse tokens = this.pollDeviceToken(deviceAuth.deviceCode);
                        if (tokens != null) {
                            if (tokens.error != null) {
                                if ("authorization_pending".equals(tokens.error)) {
                                    continue;
                                }
                                else if ("slow_down".equals(tokens.error)) {
                                    pollInterval += 5;
                                }
                                else {
                                    flow.onFailure("Device authorization failed: " + tokens.error);
                                    return;
                                }
                            }
                            else {
                                flow.onSuccess(tokens);
                                return;
                            }
                        }
                    }
                    if (cancelled.get()) {
                        flow.onFailure("Authentication cancelled");
                    }
                    else {
                        flow.onFailure("Device authorization expired");
                    }
                }
            }
            catch (final Exception e) {
                OAuthClient.LOGGER.at(Level.WARNING).withCause(e).log("OAuth device flow failed");
                if (!cancelled.get()) {
                    flow.onFailure(e.getMessage());
                }
            }
            return;
        });
        return () -> cancelled.set(true);
    }
    
    @Nullable
    public TokenResponse refreshTokens(@Nonnull final String refreshToken) {
        try {
            final String body = "grant_type=refresh_token&client_id=" + URLEncoder.encode("hytale-server", StandardCharsets.UTF_8) + "&refresh_token=" + URLEncoder.encode(refreshToken, StandardCharsets.UTF_8);
            final HttpRequest request = HttpRequest.newBuilder().uri(URI.create("https://oauth.accounts.hytale.com/oauth2/token")).header("Content-Type", "application/x-www-form-urlencoded").header("User-Agent", AuthConfig.USER_AGENT).POST(HttpRequest.BodyPublishers.ofString(body)).build();
            final HttpResponse<String> response = this.httpClient.send(request, HttpResponse.BodyHandlers.ofString());
            if (response.statusCode() != 200) {
                OAuthClient.LOGGER.at(Level.WARNING).log("Token refresh failed: HTTP %d - %s", response.statusCode(), response.body());
                return null;
            }
            return this.parseTokenResponse(response.body());
        }
        catch (final Exception e) {
            OAuthClient.LOGGER.at(Level.WARNING).withCause(e).log("Token refresh failed");
            return null;
        }
    }
    
    private String buildAuthUrl(final String state, final String codeChallenge, final String redirectUri) {
        return "https://oauth.accounts.hytale.com/oauth2/auth?response_type=code&client_id=" + URLEncoder.encode("hytale-server", StandardCharsets.UTF_8) + "&redirect_uri=" + URLEncoder.encode(redirectUri, StandardCharsets.UTF_8) + "&scope=" + URLEncoder.encode(String.join(" ", (CharSequence[])AuthConfig.SCOPES), StandardCharsets.UTF_8) + "&state=" + URLEncoder.encode(state, StandardCharsets.UTF_8) + "&code_challenge=" + URLEncoder.encode(codeChallenge, StandardCharsets.UTF_8) + "&code_challenge_method=S256";
    }
    
    @Nullable
    private TokenResponse exchangeCodeForTokens(final String code, final String codeVerifier, final String redirectUri) {
        try {
            final String body = "grant_type=authorization_code&client_id=" + URLEncoder.encode("hytale-server", StandardCharsets.UTF_8) + "&code=" + URLEncoder.encode(code, StandardCharsets.UTF_8) + "&redirect_uri=" + URLEncoder.encode(redirectUri, StandardCharsets.UTF_8) + "&code_verifier=" + URLEncoder.encode(codeVerifier, StandardCharsets.UTF_8);
            final HttpRequest request = HttpRequest.newBuilder().uri(URI.create("https://oauth.accounts.hytale.com/oauth2/token")).header("Content-Type", "application/x-www-form-urlencoded").header("User-Agent", AuthConfig.USER_AGENT).POST(HttpRequest.BodyPublishers.ofString(body)).build();
            final HttpResponse<String> response = this.httpClient.send(request, HttpResponse.BodyHandlers.ofString());
            if (response.statusCode() != 200) {
                OAuthClient.LOGGER.at(Level.WARNING).log("Token exchange failed: HTTP %d - %s", response.statusCode(), response.body());
                return null;
            }
            return this.parseTokenResponse(response.body());
        }
        catch (final Exception e) {
            OAuthClient.LOGGER.at(Level.WARNING).withCause(e).log("Token exchange failed");
            return null;
        }
    }
    
    @Nullable
    private DeviceAuthResponse requestDeviceAuthorization() {
        try {
            final String body = "client_id=" + URLEncoder.encode("hytale-server", StandardCharsets.UTF_8) + "&scope=" + URLEncoder.encode(String.join(" ", (CharSequence[])AuthConfig.SCOPES), StandardCharsets.UTF_8);
            final HttpRequest request = HttpRequest.newBuilder().uri(URI.create("https://oauth.accounts.hytale.com/oauth2/device/auth")).header("Content-Type", "application/x-www-form-urlencoded").header("User-Agent", AuthConfig.USER_AGENT).POST(HttpRequest.BodyPublishers.ofString(body)).build();
            final HttpResponse<String> response = this.httpClient.send(request, HttpResponse.BodyHandlers.ofString());
            if (response.statusCode() != 200) {
                OAuthClient.LOGGER.at(Level.WARNING).log("Device authorization request failed: HTTP %d - %s", response.statusCode(), response.body());
                return null;
            }
            return this.parseDeviceAuthResponse(response.body());
        }
        catch (final Exception e) {
            OAuthClient.LOGGER.at(Level.WARNING).withCause(e).log("Device authorization request failed");
            return null;
        }
    }
    
    @Nullable
    private TokenResponse pollDeviceToken(final String deviceCode) {
        try {
            final String body = "grant_type=urn:ietf:params:oauth:grant-type:device_code&client_id=" + URLEncoder.encode("hytale-server", StandardCharsets.UTF_8) + "&device_code=" + URLEncoder.encode(deviceCode, StandardCharsets.UTF_8);
            final HttpRequest request = HttpRequest.newBuilder().uri(URI.create("https://oauth.accounts.hytale.com/oauth2/token")).header("Content-Type", "application/x-www-form-urlencoded").header("User-Agent", AuthConfig.USER_AGENT).POST(HttpRequest.BodyPublishers.ofString(body)).build();
            final HttpResponse<String> response = this.httpClient.send(request, HttpResponse.BodyHandlers.ofString());
            if (response.statusCode() == 400) {
                return this.parseTokenResponse(response.body());
            }
            if (response.statusCode() != 200) {
                OAuthClient.LOGGER.at(Level.WARNING).log("Device token poll failed: HTTP %d - %s", response.statusCode(), response.body());
                return null;
            }
            return this.parseTokenResponse(response.body());
        }
        catch (final Exception e) {
            OAuthClient.LOGGER.at(Level.WARNING).withCause(e).log("Device token poll failed");
            return null;
        }
    }
    
    private String generateRandomString(final int length) {
        final byte[] bytes = new byte[length];
        OAuthClient.RANDOM.nextBytes(bytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes).substring(0, length);
    }
    
    private String generateCodeChallenge(final String verifier) {
        try {
            final MessageDigest digest = MessageDigest.getInstance("SHA-256");
            final byte[] hash = digest.digest(verifier.getBytes(StandardCharsets.US_ASCII));
            return Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
        }
        catch (final Exception e) {
            throw new RuntimeException("Failed to generate code challenge", e);
        }
    }
    
    private String extractParam(final String query, final String name) {
        if (query == null) {
            return null;
        }
        final Pattern pattern = Pattern.compile(name + "=([^&]*)");
        final Matcher matcher = pattern.matcher(query);
        if (matcher.find()) {
            return URLDecoder.decode(matcher.group(1), StandardCharsets.UTF_8);
        }
        return null;
    }
    
    private String encodeStateWithPort(final String state, final int port) {
        final String json = String.format("{\"state\":\"%s\",\"port\":\"%d\"}", state, port);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(json.getBytes(StandardCharsets.UTF_8));
    }
    
    private TokenResponse parseTokenResponse(final String json) {
        final JsonObject obj = JsonParser.parseString(json).getAsJsonObject();
        return new TokenResponse(getJsonString(obj, "access_token"), getJsonString(obj, "refresh_token"), getJsonString(obj, "id_token"), getJsonString(obj, "error"), getJsonInt(obj, "expires_in", 0));
    }
    
    private DeviceAuthResponse parseDeviceAuthResponse(final String json) {
        final JsonObject obj = JsonParser.parseString(json).getAsJsonObject();
        return new DeviceAuthResponse(getJsonString(obj, "device_code"), getJsonString(obj, "user_code"), getJsonString(obj, "verification_uri"), getJsonString(obj, "verification_uri_complete"), getJsonInt(obj, "expires_in", 600), getJsonInt(obj, "interval", 5));
    }
    
    @Nullable
    private static String getJsonString(final JsonObject obj, final String key) {
        final JsonElement elem = obj.get(key);
        return (elem != null && elem.isJsonPrimitive()) ? elem.getAsString() : null;
    }
    
    private static int getJsonInt(final JsonObject obj, final String key, final int defaultValue) {
        final JsonElement elem = obj.get(key);
        return (elem != null && elem.isJsonPrimitive()) ? elem.getAsInt() : defaultValue;
    }
    
    private static String buildHtmlPage(final boolean success, final String title, final String heading, final String message, @Nullable final String errorDetail) {
        final String detail = (errorDetail != null && !errorDetail.isEmpty()) ? ("<div class=\"error\">" + errorDetail + "</div>") : "";
        final String iconClass = success ? "icon-success" : "icon-error";
        final String iconSvg = success ? "<polyline points=\"20 6 9 17 4 12\"></polyline>" : "<line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line>";
        return """
               <!DOCTYPE html>
               <html lang="en">
               <head>
                   <meta charset="UTF-8">
                   <meta name="viewport" content="width=device-width, initial-scale=1.0">
                   <title>%s - Hytale</title>
                   <link rel="preconnect" href="https://fonts.googleapis.com">
                   <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
                   <link href="https://fonts.googleapis.com/css2?family=Lexend:wght@700&family=Nunito+Sans:wght@400;700&display=swap" rel="stylesheet">
                   <style>
                       * { margin: 0; padding: 0; box-sizing: border-box; }
                       html { color-scheme: dark; background: linear-gradient(180deg, #15243A, #0F1418); min-height: 100vh; }
                       body { font-family: "Nunito Sans", sans-serif; color: #b7cedd; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; }
                       .card { background: rgba(0,0,0,0.4); border: 2px solid rgba(71,81,107,0.6); border-radius: 12px; padding: 48px 40px; max-width: 420px; text-align: center; }
                       .icon { width: 64px; height: 64px; margin: 0 auto 24px; border-radius: 50%%; display: flex; align-items: center; justify-content: center; }
                       .icon svg { width: 32px; height: 32px; }
                       .icon-success { background: linear-gradient(135deg, #2d5a3d, #1e3a2a); border: 2px solid #4a9d6b; }
                       .icon-success svg { color: #6fcf97; }
                       .icon-error { background: linear-gradient(135deg, #5a2d3d, #3a1e2a); border: 2px solid #c3194c; }
                       .icon-error svg { color: #ff6b8a; }
                       h1 { font-family: "Lexend", sans-serif; font-size: 1.5rem; text-transform: uppercase; background: linear-gradient(#f5fbff, #bfe6ff); -webkit-background-clip: text; background-clip: text; color: transparent; margin-bottom: 12px; }
                       p { line-height: 1.6; }
                       .error { background: rgba(195,25,76,0.15); border: 1px solid rgba(195,25,76,0.4); border-radius: 6px; padding: 12px; margin-top: 16px; color: #ff8fa8; font-size: 0.875rem; word-break: break-word; }
                   </style>
               </head>
               <body><div class="card"><div class="icon %s"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">%s</svg></div><h1>%s</h1><p>%s</p>%s</div></body>
               </html>
               """.formatted(title, iconClass, iconSvg, heading, message, detail);
    }
    
    static {
        LOGGER = HytaleLogger.forEnclosingClass();
        RANDOM = new SecureRandom();
    }
    
    record DeviceAuthResponse(String deviceCode, String userCode, String verificationUri, String verificationUriComplete, int expiresIn, int interval) {}
    
    record TokenResponse(@Nullable String accessToken, @Nullable String refreshToken, @Nullable String idToken, @Nullable String error, int expiresIn) {
        public boolean isSuccess() {
            return this.error == null && this.accessToken != null;
        }
        
        @Nullable
        public String accessToken() {
            return this.accessToken;
        }
        
        @Nullable
        public String refreshToken() {
            return this.refreshToken;
        }
        
        @Nullable
        public String idToken() {
            return this.idToken;
        }
        
        @Nullable
        public String error() {
            return this.error;
        }
    }
}
