/*
 * CoinSwitch PRO — Spot trading client (Java).
 *
 * Single-file client with every Spot REST endpoint wrapped as a method.
 *
 * Dependencies (Gradle):
 *   implementation 'org.bouncycastle:bcprov-jdk18on:1.78'
 *   implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0'
 *
 * Usage:
 *   CoinswitchSpot cs = new CoinswitchSpot(
 *       "<your hex api key>", "<your hex secret>");
 *
 *   System.out.println(cs.serverTime());
 *   System.out.println(cs.validateKeys());
 *   Map<String, Object> body = Map.of(
 *       "side", "buy", "symbol", "BTC/USDT",
 *       "type", "limit", "price", 60000,
 *       "quantity", 0.001, "exchange", "c2c1");
 *   System.out.println(cs.createOrder(body));
 */
package com.example.coinswitch;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
import org.bouncycastle.crypto.signers.Ed25519Signer;
import org.bouncycastle.util.encoders.Hex;

import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

public class CoinswitchSpot {

    public static final String BASE_URL = "https://coinswitch.co";

    private final String apiKey;
    private final String secretKey;
    private final HttpClient http = HttpClient.newHttpClient();
    private final ObjectMapper json = new ObjectMapper();

    public CoinswitchSpot(String apiKey, String secretKey) {
        this.apiKey = apiKey;
        this.secretKey = secretKey;
    }

    // ------------------------------------------------------------------
    // Internals
    // ------------------------------------------------------------------

    public HttpResponse<String> send(
            String method, String path,
            Map<String, String> params, Map<String, Object> body) throws Exception {
        return send(method, path, params, body, true);
    }

    public HttpResponse<String> send(
            String method, String path,
            Map<String, String> params, Map<String, Object> body,
            boolean signed) throws Exception {
        method = method.toUpperCase();

        if (params != null && !params.isEmpty()) {
            StringBuilder qs = new StringBuilder();
            for (Map.Entry<String, String> e : params.entrySet()) {
                if (qs.length() > 0) qs.append("&");
                qs.append(URLEncoder.encode(e.getKey(), "UTF-8"));
                qs.append("=");
                qs.append(URLEncoder.encode(e.getValue(), "UTF-8"));
            }
            path += (path.contains("?") ? "&" : "?") + qs;
        }
        String decodedPath = URLDecoder.decode(path, "UTF-8");

        HttpRequest.Builder req = HttpRequest.newBuilder()
            .uri(new URI(BASE_URL + decodedPath))
            .header("Content-Type", "application/json");

        if (signed) {
            String epoch = String.valueOf(System.currentTimeMillis());
            String message = method + decodedPath + epoch;
            String signature = signEd25519(secretKey, message);
            req.header("X-AUTH-APIKEY", apiKey)
               .header("X-AUTH-SIGNATURE", signature)
               .header("X-AUTH-EPOCH", epoch);
        }

        String bodyJson = (body == null) ? "" : json.writeValueAsString(body);
        switch (method) {
            case "GET":    req.GET(); break;
            case "POST":   req.POST(BodyPublishers.ofString(bodyJson)); break;
            case "DELETE": req.method("DELETE", BodyPublishers.ofString(bodyJson)); break;
            case "PUT":    req.PUT(BodyPublishers.ofString(bodyJson)); break;
            default: throw new IllegalArgumentException("Unsupported method: " + method);
        }

        return http.send(req.build(), HttpResponse.BodyHandlers.ofString());
    }

    private static String signEd25519(String secretKeyHex, String message) {
        byte[] secretBytes = Hex.decode(secretKeyHex);
        Ed25519PrivateKeyParameters privateKey = new Ed25519PrivateKeyParameters(secretBytes, 0);
        Ed25519Signer signer = new Ed25519Signer();
        signer.init(true, privateKey);
        byte[] msg = message.getBytes(StandardCharsets.UTF_8);
        signer.update(msg, 0, msg.length);
        return Hex.toHexString(signer.generateSignature());
    }

    // ------------------------------------------------------------------
    // Endpoint methods
    // ------------------------------------------------------------------

    public String serverTime() throws Exception {
        return send("GET", "/trade/api/v2/time", null, null, false).body();
    }

    public String validateKeys() throws Exception {
        return send("GET", "/trade/api/v2/validate/keys", null, null).body();
    }

    public String ping() throws Exception {
        return send("GET", "/trade/api/v2/ping", null, null).body();
    }

    public String activeCoins(String exchange) throws Exception {
        return send("GET", "/trade/api/v2/coins",
            Map.of("exchange", exchange), null).body();
    }

    public String exchangePrecision(String exchange, String symbol) throws Exception {
        return send("POST", "/trade/api/v2/exchangePrecision",
            null, Map.of("exchange", exchange, "symbol", symbol)).body();
    }

    public String tradeInfo(String exchange, String symbolOrNull) throws Exception {
        Map<String, String> params = new HashMap<>();
        params.put("exchange", exchange);
        if (symbolOrNull != null) params.put("symbol", symbolOrNull);
        return send("GET", "/trade/api/v2/tradeInfo", params, null).body();
    }

    public String tradingFee(String exchange) throws Exception {
        return send("GET", "/trade/api/v2/tradingFee",
            Map.of("exchange", exchange), null).body();
    }

    public String portfolio() throws Exception {
        return send("GET", "/trade/api/v2/user/portfolio", null, null).body();
    }

    public String tds() throws Exception {
        return send("GET", "/trade/api/v2/tds", null, null).body();
    }

    /**
     * Place a spot order. body must include side / symbol / type / price /
     * quantity / exchange. We auto-mint a client_order_id if the caller
     * doesn't pass one (use it as your idempotency key on retries).
     */
    public String createOrder(Map<String, Object> body) throws Exception {
        Map<String, Object> b = new HashMap<>(body);
        b.putIfAbsent("client_order_id", UUID.randomUUID().toString());
        return send("POST", "/trade/api/v2/order", null, b).body();
    }

    public String cancelOrder(String orderId) throws Exception {
        return send("DELETE", "/trade/api/v2/order", null,
            Map.of("order_id", orderId)).body();
    }

    public String getOrder(String orderId) throws Exception {
        return send("GET", "/trade/api/v2/order",
            Map.of("order_id", orderId), null).body();
    }

    public String listOrders(boolean open, Map<String, String> filters) throws Exception {
        Map<String, String> p = new HashMap<>();
        p.put("open", String.valueOf(open));
        if (filters != null) p.putAll(filters);
        return send("GET", "/trade/api/v2/orders", p, null).body();
    }

    public String depth(String exchange, String symbol) throws Exception {
        return send("GET", "/trade/api/v2/depth",
            Map.of("exchange", exchange, "symbol", symbol), null).body();
    }

    public String candles(String exchange, String symbol, int interval,
                          long startTime, long endTime) throws Exception {
        return send("GET", "/trade/api/v2/candles", Map.of(
            "exchange", exchange,
            "symbol", symbol,
            "interval", String.valueOf(interval),
            "start_time", String.valueOf(startTime),
            "end_time", String.valueOf(endTime)
        ), null).body();
    }

    public String trades(String exchange, String symbol) throws Exception {
        return send("GET", "/trade/api/v2/trades",
            Map.of("exchange", exchange, "symbol", symbol), null).body();
    }

    public String tickerAllPairs(String exchange) throws Exception {
        return send("GET", "/trade/api/v2/24hr/all-pairs/ticker",
            Map.of("exchange", exchange), null).body();
    }

    public String ticker(String symbol, String exchange) throws Exception {
        return send("GET", "/trade/api/v2/24hr/ticker",
            Map.of("symbol", symbol, "exchange", exchange), null).body();
    }

    // ------------------------------------------------------------------
    // Smoke test
    // ------------------------------------------------------------------

    public static void main(String[] args) throws Exception {
        CoinswitchSpot cs = new CoinswitchSpot(
            "<your api key>", "<your secret key>");
        System.out.println(cs.serverTime());
    }
}
