package io.synor.ibc;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import io.synor.ibc.Types.*;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.CompletableFuture;

/**
 * Synor IBC SDK for Java
 *
 * Inter-Blockchain Communication (IBC) protocol for cross-chain interoperability.
 */
public class SynorIbc implements AutoCloseable {
    private final IbcConfig config;
    private final HttpClient httpClient;
    private final Gson gson;
    private volatile boolean closed = false;

    public final LightClientClient clients;
    public final ConnectionsClient connections;
    public final ChannelsClient channels;
    public final TransferClient transfer;
    public final SwapsClient swaps;

    public SynorIbc(IbcConfig config) {
        this.config = config;
        this.httpClient = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(config.getTimeout()))
            .build();
        this.gson = new Gson();

        this.clients = new LightClientClient(this);
        this.connections = new ConnectionsClient(this);
        this.channels = new ChannelsClient(this);
        this.transfer = new TransferClient(this);
        this.swaps = new SwapsClient(this);
    }

    public String getChainId() {
        return config.getChainId();
    }

    public CompletableFuture<Map<String, Object>> getChainInfo() {
        return get("/chain");
    }

    public CompletableFuture<Height> getHeight() {
        return get("/chain/height").thenApply(Height::fromJson);
    }

    public CompletableFuture<Boolean> healthCheck() {
        return get("/health")
            .thenApply(result -> "healthy".equals(result.get("status")))
            .exceptionally(e -> false);
    }

    @Override
    public void close() {
        closed = true;
    }

    public boolean isClosed() {
        return closed;
    }

    // Internal HTTP methods
    CompletableFuture<Map<String, Object>> get(String path) {
        checkClosed();
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(config.getEndpoint() + path))
            .headers(getHeaders())
            .GET()
            .build();
        return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
            .thenApply(this::handleResponse);
    }

    CompletableFuture<Map<String, Object>> post(String path, Map<String, Object> body) {
        checkClosed();
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(config.getEndpoint() + path))
            .headers(getHeaders())
            .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(body)))
            .build();
        return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
            .thenApply(this::handleResponse);
    }

    private String[] getHeaders() {
        return new String[] {
            "Content-Type", "application/json",
            "Authorization", "Bearer " + config.getApiKey(),
            "X-SDK-Version", "java/0.1.0",
            "X-Chain-Id", config.getChainId()
        };
    }

    private Map<String, Object> handleResponse(HttpResponse<String> response) {
        if (response.statusCode() >= 400) {
            Map<String, Object> error = null;
            try {
                error = gson.fromJson(response.body(),
                    new TypeToken<Map<String, Object>>(){}.getType());
            } catch (Exception ignored) {}
            throw new IbcException(
                error != null ? (String) error.get("message") : "HTTP " + response.statusCode(),
                error != null ? (String) error.get("code") : null,
                response.statusCode()
            );
        }
        return gson.fromJson(response.body(), new TypeToken<Map<String, Object>>(){}.getType());
    }

    private void checkClosed() {
        if (closed) {
            throw new IbcException("Client has been closed", "CLIENT_CLOSED", null);
        }
    }

    /**
     * Light client sub-client
     */
    public static class LightClientClient {
        private final SynorIbc ibc;

        LightClientClient(SynorIbc ibc) {
            this.ibc = ibc;
        }

        public CompletableFuture<ClientId> create(ClientType clientType,
                                                   ClientState clientState,
                                                   Map<String, Object> consensusState) {
            Map<String, Object> body = new HashMap<>();
            body.put("client_type", clientType.getValue());
            body.put("client_state", clientState.toJson());
            body.put("consensus_state", consensusState);
            return ibc.post("/clients", body)
                .thenApply(result -> new ClientId((String) result.get("client_id")));
        }

        @SuppressWarnings("unchecked")
        public CompletableFuture<ClientState> getState(ClientId clientId) {
            return ibc.get("/clients/" + clientId.getId() + "/state")
                .thenApply(ClientState::fromJson);
        }

        @SuppressWarnings("unchecked")
        public CompletableFuture<List<Map<String, Object>>> list() {
            return ibc.get("/clients")
                .thenApply(result -> (List<Map<String, Object>>) result.getOrDefault("clients", List.of()));
        }
    }

    /**
     * Connections sub-client
     */
    public static class ConnectionsClient {
        private final SynorIbc ibc;

        ConnectionsClient(SynorIbc ibc) {
            this.ibc = ibc;
        }

        public CompletableFuture<ConnectionId> openInit(ClientId clientId,
                                                         ClientId counterpartyClientId) {
            Map<String, Object> body = new HashMap<>();
            body.put("client_id", clientId.getId());
            body.put("counterparty_client_id", counterpartyClientId.getId());
            return ibc.post("/connections/init", body)
                .thenApply(result -> new ConnectionId((String) result.get("connection_id")));
        }

        public CompletableFuture<Map<String, Object>> get(ConnectionId connectionId) {
            return ibc.get("/connections/" + connectionId.getId());
        }

        @SuppressWarnings("unchecked")
        public CompletableFuture<List<Map<String, Object>>> list() {
            return ibc.get("/connections")
                .thenApply(result -> (List<Map<String, Object>>) result.getOrDefault("connections", List.of()));
        }
    }

    /**
     * Channels sub-client
     */
    public static class ChannelsClient {
        private final SynorIbc ibc;

        ChannelsClient(SynorIbc ibc) {
            this.ibc = ibc;
        }

        public CompletableFuture<Void> bindPort(PortId portId, String module) {
            Map<String, Object> body = new HashMap<>();
            body.put("port_id", portId.getId());
            body.put("module", module);
            return ibc.post("/ports/bind", body).thenApply(r -> null);
        }

        public CompletableFuture<ChannelId> openInit(PortId portId,
                                                      ChannelOrder ordering,
                                                      ConnectionId connectionId,
                                                      PortId counterpartyPort,
                                                      String version) {
            Map<String, Object> body = new HashMap<>();
            body.put("port_id", portId.getId());
            body.put("ordering", ordering.name().toLowerCase());
            body.put("connection_id", connectionId.getId());
            body.put("counterparty_port", counterpartyPort.getId());
            body.put("version", version);
            return ibc.post("/channels/init", body)
                .thenApply(result -> new ChannelId((String) result.get("channel_id")));
        }

        public CompletableFuture<Map<String, Object>> get(PortId portId, ChannelId channelId) {
            return ibc.get("/channels/" + portId.getId() + "/" + channelId.getId());
        }

        @SuppressWarnings("unchecked")
        public CompletableFuture<List<Map<String, Object>>> list() {
            return ibc.get("/channels")
                .thenApply(result -> (List<Map<String, Object>>) result.getOrDefault("channels", List.of()));
        }
    }

    /**
     * Transfer sub-client (ICS-20)
     */
    public static class TransferClient {
        private final SynorIbc ibc;

        TransferClient(SynorIbc ibc) {
            this.ibc = ibc;
        }

        public CompletableFuture<Map<String, Object>> transfer(String sourcePort,
                                                                String sourceChannel,
                                                                String denom,
                                                                String amount,
                                                                String sender,
                                                                String receiver,
                                                                Timeout timeout,
                                                                String memo) {
            Map<String, Object> body = new HashMap<>();
            body.put("source_port", sourcePort);
            body.put("source_channel", sourceChannel);
            body.put("token", Map.of("denom", denom, "amount", amount));
            body.put("sender", sender);
            body.put("receiver", receiver);
            if (timeout != null) {
                body.put("timeout_height", timeout.getHeight().toJson());
                body.put("timeout_timestamp", timeout.getTimestamp().toString());
            }
            if (memo != null) body.put("memo", memo);
            return ibc.post("/transfer", body);
        }

        public CompletableFuture<Map<String, Object>> getDenomTrace(String ibcDenom) {
            return ibc.get("/transfer/denom_trace/" + ibcDenom);
        }
    }

    /**
     * Swaps sub-client (HTLC)
     */
    public static class SwapsClient {
        private final SynorIbc ibc;

        SwapsClient(SynorIbc ibc) {
            this.ibc = ibc;
        }

        public CompletableFuture<Map<String, Object>> initiate(String responder,
                                                                Map<String, Object> initiatorAsset,
                                                                Map<String, Object> responderAsset) {
            Map<String, Object> body = new HashMap<>();
            body.put("responder", responder);
            body.put("initiator_asset", initiatorAsset);
            body.put("responder_asset", responderAsset);
            return ibc.post("/swaps/initiate", body);
        }

        public CompletableFuture<Void> lock(SwapId swapId) {
            return ibc.post("/swaps/" + swapId.getId() + "/lock", Map.of())
                .thenApply(r -> null);
        }

        public CompletableFuture<Map<String, Object>> respond(SwapId swapId,
                                                               Map<String, Object> asset) {
            return ibc.post("/swaps/" + swapId.getId() + "/respond", Map.of("asset", asset));
        }

        public CompletableFuture<Map<String, Object>> claim(SwapId swapId, byte[] secret) {
            return ibc.post("/swaps/" + swapId.getId() + "/claim",
                Map.of("secret", Base64.getEncoder().encodeToString(secret)));
        }

        public CompletableFuture<Map<String, Object>> refund(SwapId swapId) {
            return ibc.post("/swaps/" + swapId.getId() + "/refund", Map.of());
        }

        public CompletableFuture<AtomicSwap> get(SwapId swapId) {
            return ibc.get("/swaps/" + swapId.getId())
                .thenApply(AtomicSwap::fromJson);
        }

        @SuppressWarnings("unchecked")
        public CompletableFuture<List<AtomicSwap>> listActive() {
            return ibc.get("/swaps/active")
                .thenApply(result -> {
                    List<Map<String, Object>> swaps =
                        (List<Map<String, Object>>) result.getOrDefault("swaps", List.of());
                    return swaps.stream().map(AtomicSwap::fromJson).toList();
                });
        }
    }
}
