import java.net.URI;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.security.MessageDigest;
import java.time.Duration;
import java.util.*;

// Java 17+, no external dependencies. Default: print request only.
public class WalletRequest {
    static final String URL = "https://transaction.hisspay.com/transaction/payin/v1/apply";
    static String quote(String s) {
        StringBuilder b = new StringBuilder("\"");
        for (char c : s.toCharArray()) {
            switch (c) {
                case '"': b.append("\\\""); break;
                case '\\': b.append("\\\\"); break;
                case '\n': b.append("\\n"); break;
                case '\r': b.append("\\r"); break;
                case '\t': b.append("\\t"); break;
                default: if (c < 32) b.append(String.format("\\u%04x", (int)c)); else b.append(c);
            }
        }
        return b.append('"').toString();
    }
    static String json(Map<String, ?> values) {
        StringJoiner j = new StringJoiner(",", "{", "}");
        values.forEach((k, v) -> j.add(quote(k) + ":" +
            (v instanceof Map ? json((Map<String, ?>)v) : v instanceof Long ? v.toString() : quote((String)v))));
        return j.toString();
    }
    static String required(Properties p, String k) {
        String v = p.getProperty(k);
        if (v == null || v.isBlank()) throw new IllegalArgumentException("Missing " + k);
        return v;
    }
    static Map<String, String> body(String wallet, Properties p) {
        if (!wallet.equals("google") && !wallet.equals("apple")) throw new IllegalArgumentException("Use google or apple");
        Map<String, Object> connection = new LinkedHashMap<>();
        connection.put("country", "AU");
        connection.put("state", required(p, "state"));
        connection.put("zip_code", required(p, "zip"));
        connection.put("media_source", "website");
        Map<String, Object> account = new LinkedHashMap<>();
        account.put("merchant_user_id", required(p, "userId"));
        account.put("create_time", Long.parseLong(required(p, "created")));
        account.put("role", required(p, "role"));
        account.put("area_code", "61");
        account.put("first_name", required(p, "first"));
        account.put("last_name", required(p, "last"));
        Map<String, Object> context = new LinkedHashMap<>();
        context.put("connection_info", connection); context.put("account_info", account);
        Map<String, String> b = new LinkedHashMap<>();
        for (String k : List.of("merchantOrderNo", "amount", "name", "phone", "email", "clientIp", "notifyUrl", "returnUrl"))
            b.put(k, required(p, k));
        b.put("currency", "AUD");
        b.put("product", wallet.equals("google") ? "AUD-PAYPLUS-GOOGLE-M85" : "AUD-PAYPLUS-APPLE-M85");
        b.put("payType", wallet.equals("google") ? "GOOGLE_PAY" : "APPLE_PAY");
        b.put("cardExtra", json(context)); // String, NOT a nested JSON object.
        return b;
    }
    static Map<String, String> sign(Map<String, String> body, String appId, String secret, String timestamp, String nonce) throws Exception {
        Map<String, String> headers = new LinkedHashMap<>();
        headers.put("appId", appId); headers.put("timestamp", timestamp); headers.put("nonce", nonce);
        TreeMap<String, String> values = new TreeMap<>(body); values.putAll(headers);
        StringJoiner canonical = new StringJoiner("&");
        values.forEach((k, v) -> canonical.add(k + "=" + v));
        byte[] digest = MessageDigest.getInstance("SHA-256").digest((canonical.toString() + secret).getBytes(StandardCharsets.UTF_8));
        headers.put("sign", HexFormat.of().formatHex(digest));
        return headers;
    }
    public static void main(String[] args) throws Exception {
        if (args.length < 2 || args.length > 3 || (args.length == 3 && !args[2].equals("--send")))
            throw new IllegalArgumentException("Usage: java WalletRequest google|apple payer.properties [--send]");
        Properties p = new Properties();
        try (var reader = Files.newBufferedReader(Path.of(args[1]), StandardCharsets.UTF_8)) { p.load(reader); }
        Map<String, String> b = body(args[0], p);
        String payload = json(b);
        if (args.length == 2) { System.out.println(payload); return; }
        if (b.get("clientIp").equals("<PAYER_PUBLIC_IP>") || b.get("merchantOrderNo").contains("REPLACE_UNIQUE"))
            throw new IllegalArgumentException("Replace example payer IP and unique order number first");
        String appId = Objects.requireNonNull(System.getenv("HISSPAY_APP_ID"), "HISSPAY_APP_ID");
        String secret = Objects.requireNonNull(System.getenv("HISSPAY_APP_SECRET"), "HISSPAY_APP_SECRET");
        Map<String, String> headers = sign(b, appId, secret, Long.toString(System.currentTimeMillis()), UUID.randomUUID().toString().replace("-", ""));
        HttpRequest.Builder req = HttpRequest.newBuilder(URI.create(URL)).timeout(Duration.ofSeconds(30))
            .header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(payload, StandardCharsets.UTF_8));
        headers.forEach(req::header);
        HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).followRedirects(HttpClient.Redirect.NEVER).build();
        // Send once. On timeout/error, query the original order; do not automatically resubmit.
        HttpResponse<String> response = client.send(req.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
        System.out.println("HTTP " + response.statusCode()); System.out.println(response.body());
    }
}
