package io.synor.rpc;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.WebSocket;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

/**
 * Synor RPC SDK for Java.
 *
 * Query blocks, transactions, and chain state with WebSocket subscription support.
 *
 * Example:
 * <pre>{@code
 * SynorRpc rpc = new SynorRpc("your-api-key");
 * Block block = rpc.getLatestBlock().join();
 * System.out.println("Height: " + block.getHeight());
 * }</pre>
 */
public class SynorRpc implements AutoCloseable {
    private static final String DEFAULT_ENDPOINT = "https://rpc.synor.cc/api/v1";
    private static final String DEFAULT_WS_ENDPOINT = "wss://rpc.synor.cc/ws";

    private final RpcConfig config;
    private final HttpClient httpClient;
    private final Gson gson;
    private WebSocket webSocket;
    private final Map<String, Consumer<String>> subscriptions = new ConcurrentHashMap<>();

    public SynorRpc(String apiKey) {
        this(RpcConfig.builder().apiKey(apiKey).build());
    }

    public SynorRpc(RpcConfig config) {
        this.config = config;
        this.httpClient = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(config.getTimeoutSeconds()))
            .build();
        this.gson = new GsonBuilder()
            .setFieldNamingPolicy(com.google.gson.FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
            .create();
    }

    /** Get a block by hash. */
    public CompletableFuture<Block> getBlockByHash(String hash) {
        return get("/blocks/" + hash, Block.class);
    }

    /** Get a block by height. */
    public CompletableFuture<Block> getBlockByHeight(long height) {
        return get("/blocks/height/" + height, Block.class);
    }

    /** Get the latest block. */
    public CompletableFuture<Block> getLatestBlock() {
        return get("/blocks/latest", Block.class);
    }

    /** Get a block header by hash. */
    public CompletableFuture<BlockHeader> getBlockHeaderByHash(String hash) {
        return get("/blocks/" + hash + "/header", BlockHeaderResponse.class)
            .thenApply(BlockHeaderResponse::getHeader);
    }

    /** Get a transaction by ID. */
    public CompletableFuture<Transaction> getTransaction(String txid) {
        return get("/transactions/" + txid, Transaction.class);
    }

    /** Send a raw transaction. */
    public CompletableFuture<String> sendRawTransaction(String rawTx) {
        var body = Map.of("raw", rawTx);
        return post("/transactions", body, SendTxResponse.class)
            .thenApply(SendTxResponse::getTxid);
    }

    /** Estimate transaction fee. */
    public CompletableFuture<FeeEstimate> estimateFee(Priority priority) {
        return get("/fees/estimate?priority=" + priority.getValue(), FeeEstimate.class);
    }

    /** Get chain information. */
    public CompletableFuture<ChainInfo> getChainInfo() {
        return get("/chain", ChainInfo.class);
    }

    /** Get mempool information. */
    public CompletableFuture<MempoolInfo> getMempoolInfo() {
        return get("/mempool", MempoolInfo.class);
    }

    /** Get UTXOs for an address. */
    public CompletableFuture<UTXO[]> getUTXOs(String address) {
        return get("/addresses/" + address + "/utxos", UTXOsResponse.class)
            .thenApply(UTXOsResponse::getUtxos);
    }

    /** Get balance for an address. */
    public CompletableFuture<Balance> getBalance(String address) {
        return get("/addresses/" + address + "/balance", Balance.class);
    }

    /** Subscribe to new blocks. */
    public CompletableFuture<Subscription> subscribeBlocks(Consumer<Block> callback) {
        return subscribe("blocks", null, data -> {
            Block block = gson.fromJson(data, BlockNotification.class).getBlock();
            callback.accept(block);
        });
    }

    /** Subscribe to address transactions. */
    public CompletableFuture<Subscription> subscribeAddress(String address, Consumer<Transaction> callback) {
        return subscribe("address", Map.of("address", address), data -> {
            Transaction tx = gson.fromJson(data, TransactionNotification.class).getTransaction();
            callback.accept(tx);
        });
    }

    private CompletableFuture<Subscription> subscribe(String type, Map<String, String> params, Consumer<String> handler) {
        return ensureWebSocket().thenApply(ws -> {
            String subId = type + "_" + System.currentTimeMillis();
            subscriptions.put(subId, handler);

            var msg = new java.util.HashMap<String, Object>();
            msg.put("type", "subscribe");
            msg.put("id", subId);
            msg.put("subscription", type);
            if (params != null) msg.putAll(params);

            ws.sendText(gson.toJson(msg), true);

            return new Subscription(subId, type, System.currentTimeMillis(), () -> {
                subscriptions.remove(subId);
                ws.sendText(gson.toJson(Map.of("type", "unsubscribe", "id", subId)), true);
            });
        });
    }

    private CompletableFuture<WebSocket> ensureWebSocket() {
        if (webSocket != null) {
            return CompletableFuture.completedFuture(webSocket);
        }

        String wsUrl = config.getWsEndpoint() + "?apiKey=" + config.getApiKey() + "&network=" + config.getNetwork().getValue();

        return httpClient.newWebSocketBuilder()
            .buildAsync(URI.create(wsUrl), new WebSocket.Listener() {
                @Override
                public CompletionStage<?> onText(WebSocket ws, CharSequence data, boolean last) {
                    var msg = gson.fromJson(data.toString(), WsMessage.class);
                    if (msg.subscriptionId != null && subscriptions.containsKey(msg.subscriptionId)) {
                        subscriptions.get(msg.subscriptionId).accept(data.toString());
                    }
                    return WebSocket.Listener.super.onText(ws, data, last);
                }
            })
            .thenApply(ws -> {
                this.webSocket = ws;
                return ws;
            });
    }

    private <T> CompletableFuture<T> get(String path, Class<T> responseType) {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(config.getEndpoint() + path))
            .header("Authorization", "Bearer " + config.getApiKey())
            .header("Content-Type", "application/json")
            .header("X-Network", config.getNetwork().getValue())
            .timeout(Duration.ofSeconds(config.getTimeoutSeconds()))
            .GET()
            .build();

        return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
            .thenApply(response -> handleResponse(response, responseType));
    }

    private <T> CompletableFuture<T> post(String path, Object body, Class<T> responseType) {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(config.getEndpoint() + path))
            .header("Authorization", "Bearer " + config.getApiKey())
            .header("Content-Type", "application/json")
            .header("X-Network", config.getNetwork().getValue())
            .timeout(Duration.ofSeconds(config.getTimeoutSeconds()))
            .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(body)))
            .build();

        return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
            .thenApply(response -> handleResponse(response, responseType));
    }

    private <T> T handleResponse(HttpResponse<String> response, Class<T> responseType) {
        if (response.statusCode() >= 400) {
            ErrorResponse error = gson.fromJson(response.body(), ErrorResponse.class);
            throw new RpcException(
                error != null ? error.getMessage() : "Unknown error",
                response.statusCode(),
                error != null ? error.getCode() : null
            );
        }
        return gson.fromJson(response.body(), responseType);
    }

    @Override
    public void close() {
        if (webSocket != null) {
            webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "closing");
        }
    }

    // Internal types
    private static class WsMessage { String subscriptionId; String data; }
    private record BlockHeaderResponse(BlockHeader header) { BlockHeader getHeader() { return header; } }
    private record SendTxResponse(String txid) { String getTxid() { return txid; } }
    private record UTXOsResponse(UTXO[] utxos) { UTXO[] getUtxos() { return utxos; } }
    private record BlockNotification(Block block) { Block getBlock() { return block; } }
    private record TransactionNotification(Transaction transaction) { Transaction getTransaction() { return transaction; } }
    private record ErrorResponse(String message, String code) { String getMessage() { return message; } String getCode() { return code; } }
}
