// One GET with java.net.http.HttpClient for the loopback compression lab ("command" cell kind).
//
//   java JavaHttpClientLab.java --url URL --out FILE [--proxy URL] [--cafile CA.pem]
//        [--accept-encoding VALUE] [--gunzip]
//
// Run with the single-file source launcher (JEP 330); no build step.
// --cafile CA.pem       trusts ONLY the lab CA, through an explicit SSLContext built from that
//                       PEM (KeyStore + TrustManagerFactory). Certificate and hostname
//                       verification stay on. Without --cafile the JDK's default cacerts is used.
// --proxy URL           ProxySelector.of(host:port): absolute-form for http://, CONNECT for https://.
// --accept-encoding V   adds "Accept-Encoding: V" to the request.
// --gunzip              the application decodes a gzip body itself with GZIPInputStream
//                       (HttpClient does not decode Content-Encoding).
// Writes the body handed to the application to --out and prints one LABRESULT line.

import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.ProxySelector;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;

public class JavaHttpClientLab {

    public static void main(String[] argv) throws Exception {
        Map<String, String> a = new LinkedHashMap<>();
        for (int i = 0; i < argv.length; i++) {
            String k = argv[i];
            if (!k.startsWith("--")) {
                System.err.println("JavaHttpClientLab: unexpected argument " + k);
                System.exit(2);
            }
            if (i + 1 < argv.length && !argv[i + 1].startsWith("--")) {
                a.put(k.substring(2), argv[++i]);
            } else {
                a.put(k.substring(2), "true");
            }
        }
        a.values().removeIf(String::isEmpty);
        if (!a.containsKey("url") || !a.containsKey("out")) {
            System.err.println("JavaHttpClientLab: --url and --out are required");
            System.exit(2);
        }

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("client_version", "java.net.http.HttpClient (JDK " + System.getProperty("java.runtime.version") + ")");
        result.put("runtime", "Java");
        result.put("runtime_version", System.getProperty("java.vm.name") + " " + System.getProperty("java.runtime.version")
                + " (" + System.getProperty("java.vendor.version", System.getProperty("java.vendor")) + ", "
                + System.getProperty("os.name") + " " + System.getProperty("os.arch") + ")");
        result.put("executable_path", ProcessHandle.current().info().command().orElse(null));
        result.put("exception_text", null);
        Map<String, Object> extra = new LinkedHashMap<>();
        extra.put("java_home", System.getProperty("java.home"));
        extra.put("java_options", Map.of(
                "proxy", a.containsKey("proxy") ? "ProxySelector.of(counting proxy)" : "none",
                "sslContext", a.containsKey("cafile") ? "lab CA only (KeyStore + TrustManagerFactory)" : "JDK default",
                "Accept-Encoding", a.getOrDefault("accept-encoding", "(not set)"),
                "application_gunzip", a.containsKey("gunzip") ? "yes" : "no"));
        result.put("extra", extra);

        int exit = 0;
        try {
            HttpClient.Builder b = HttpClient.newBuilder();
            if (a.containsKey("proxy")) {
                URI p = URI.create(a.get("proxy"));
                b.proxy(ProxySelector.of(new InetSocketAddress(p.getHost(), p.getPort())));
            }
            if (a.containsKey("cafile")) {
                X509Certificate ca;
                try (InputStream in = new FileInputStream(a.get("cafile"))) {
                    ca = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(in);
                }
                KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
                ks.load(null, null);
                ks.setCertificateEntry("lab-ca", ca);
                TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
                tmf.init(ks);
                SSLContext ctx = SSLContext.getInstance("TLS");
                ctx.init(null, tmf.getTrustManagers(), null);
                b.sslContext(ctx);
            }
            HttpClient client = b.build();
            extra.put("java_client_version_setting", String.valueOf(client.version()));

            HttpRequest.Builder rb = HttpRequest.newBuilder(URI.create(a.get("url"))).GET();
            if (a.containsKey("accept-encoding")) {
                rb.header("Accept-Encoding", a.get("accept-encoding"));
            }
            HttpResponse<byte[]> r = client.send(rb.build(), HttpResponse.BodyHandlers.ofByteArray());
            byte[] body = r.body();
            String ce = r.headers().firstValue("Content-Encoding").orElse(null);
            extra.put("java_status", r.statusCode());
            extra.put("java_response_version", String.valueOf(r.version()));
            extra.put("java_content_encoding_header", ce);
            extra.put("java_body_bytes_received", body.length);
            if (a.containsKey("gunzip") && "gzip".equalsIgnoreCase(ce)) {
                try (GZIPInputStream gz = new GZIPInputStream(new ByteArrayInputStream(body))) {
                    body = gz.readAllBytes();
                }
                extra.put("java_application_gunzip_applied", true);
            }
            Files.write(Path.of(a.get("out")), body);
        } catch (Exception e) {
            String msg = e.getClass().getName() + ": " + e.getMessage();
            Throwable c = e.getCause();
            if (c != null && c != e && !String.valueOf(c.getMessage()).equals(String.valueOf(e.getMessage()))) {
                msg += " (cause: " + c.getClass().getName() + ": " + c.getMessage() + ")";
            }
            result.put("exception_text", msg);
            exit = 1;
        }
        System.out.println("LABRESULT " + json(result));
        System.exit(exit);
    }

    // Minimal JSON writer for the LABRESULT line (strings, numbers, booleans, null, maps).
    static String json(Object v) {
        if (v == null) return "null";
        if (v instanceof Number || v instanceof Boolean) return v.toString();
        if (v instanceof Map<?, ?> m) {
            StringBuilder sb = new StringBuilder("{");
            boolean first = true;
            for (Map.Entry<?, ?> e : m.entrySet()) {
                if (!first) sb.append(',');
                first = false;
                sb.append(json(String.valueOf(e.getKey()))).append(':').append(json(e.getValue()));
            }
            return sb.append('}').toString();
        }
        String s = v.toString();
        StringBuilder sb = new StringBuilder("\"");
        for (char ch : s.toCharArray()) {
            switch (ch) {
                case '"' -> sb.append("\\\"");
                case '\\' -> sb.append("\\\\");
                case '\n' -> sb.append("\\n");
                case '\r' -> sb.append("\\r");
                case '\t' -> sb.append("\\t");
                default -> {
                    if (ch < 0x20) sb.append(String.format("\\u%04x", (int) ch));
                    else sb.append(ch);
                }
            }
        }
        return sb.append('"').toString();
    }
}
