package io.synor.bridge;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;

import java.lang.reflect.Type;
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.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Synor Bridge SDK for Java.
 * Cross-chain asset transfers with lock-mint and burn-unlock patterns.
 */
public class SynorBridge implements AutoCloseable {
    private static final String DEFAULT_ENDPOINT = "https://bridge.synor.io/v1";
    private static final Gson gson = new GsonBuilder().create();
    private static final Set<TransferStatus> FINAL_STATUSES = Set.of(
            TransferStatus.completed, TransferStatus.failed, TransferStatus.refunded
    );

    private final BridgeConfig config;
    private final HttpClient httpClient;
    private final AtomicBoolean closed = new AtomicBoolean(false);

    public SynorBridge(String apiKey) {
        this(new BridgeConfig(apiKey, DEFAULT_ENDPOINT, 60, 3, false));
    }

    public SynorBridge(BridgeConfig config) {
        this.config = config;
        this.httpClient = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(config.getTimeoutSecs()))
                .build();
    }

    // ==================== Chain Operations ====================

    public CompletableFuture<List<Chain>> getSupportedChains() {
        return request("GET", "/chains", null)
                .thenApply(resp -> {
                    Type type = new TypeToken<ChainsResponse>(){}.getType();
                    ChainsResponse result = gson.fromJson(resp, type);
                    return result.chains;
                });
    }

    public CompletableFuture<Chain> getChain(ChainId chainId) {
        return request("GET", "/chains/" + chainId.name().toLowerCase(), null)
                .thenApply(resp -> gson.fromJson(resp, Chain.class));
    }

    public CompletableFuture<Boolean> isChainSupported(ChainId chainId) {
        return getChain(chainId)
                .thenApply(Chain::isSupported)
                .exceptionally(e -> false);
    }

    // ==================== Asset Operations ====================

    public CompletableFuture<List<Asset>> getSupportedAssets(ChainId chainId) {
        return request("GET", "/chains/" + chainId.name().toLowerCase() + "/assets", null)
                .thenApply(resp -> {
                    Type type = new TypeToken<AssetsResponse>(){}.getType();
                    AssetsResponse result = gson.fromJson(resp, type);
                    return result.assets;
                });
    }

    public CompletableFuture<Asset> getAsset(String assetId) {
        return request("GET", "/assets/" + encode(assetId), null)
                .thenApply(resp -> gson.fromJson(resp, Asset.class));
    }

    public CompletableFuture<WrappedAsset> getWrappedAsset(String originalAssetId, ChainId targetChain) {
        String path = "/assets/" + encode(originalAssetId) + "/wrapped/" + targetChain.name().toLowerCase();
        return request("GET", path, null)
                .thenApply(resp -> gson.fromJson(resp, WrappedAsset.class));
    }

    // ==================== Fee & Rate Operations ====================

    public CompletableFuture<FeeEstimate> estimateFee(String asset, String amount, ChainId sourceChain, ChainId targetChain) {
        Map<String, Object> body = new HashMap<>();
        body.put("asset", asset);
        body.put("amount", amount);
        body.put("sourceChain", sourceChain.name().toLowerCase());
        body.put("targetChain", targetChain.name().toLowerCase());
        return request("POST", "/fees/estimate", body)
                .thenApply(resp -> gson.fromJson(resp, FeeEstimate.class));
    }

    public CompletableFuture<ExchangeRate> getExchangeRate(String fromAsset, String toAsset) {
        String path = "/rates/" + encode(fromAsset) + "/" + encode(toAsset);
        return request("GET", path, null)
                .thenApply(resp -> gson.fromJson(resp, ExchangeRate.class));
    }

    // ==================== Lock-Mint Flow ====================

    public CompletableFuture<LockReceipt> lock(String asset, String amount, ChainId targetChain, LockOptions options) {
        Map<String, Object> body = new HashMap<>();
        body.put("asset", asset);
        body.put("amount", amount);
        body.put("targetChain", targetChain.name().toLowerCase());
        if (options != null) {
            if (options.getRecipient() != null) body.put("recipient", options.getRecipient());
            if (options.getDeadline() != null) body.put("deadline", options.getDeadline());
            if (options.getSlippage() != null) body.put("slippage", options.getSlippage());
        }
        return request("POST", "/transfers/lock", body)
                .thenApply(resp -> gson.fromJson(resp, LockReceipt.class));
    }

    public CompletableFuture<LockProof> getLockProof(String lockReceiptId) {
        return request("GET", "/transfers/lock/" + encode(lockReceiptId) + "/proof", null)
                .thenApply(resp -> gson.fromJson(resp, LockProof.class));
    }

    public CompletableFuture<LockProof> waitForLockProof(String lockReceiptId, long pollIntervalMs, long maxWaitMs) {
        long deadline = System.currentTimeMillis() + maxWaitMs;
        return waitForLockProofInternal(lockReceiptId, pollIntervalMs, deadline);
    }

    private CompletableFuture<LockProof> waitForLockProofInternal(String id, long pollMs, long deadline) {
        if (System.currentTimeMillis() >= deadline) {
            return CompletableFuture.failedFuture(
                    new BridgeException("Timeout waiting for lock proof", "CONFIRMATIONS_PENDING", 0));
        }
        return getLockProof(id)
                .exceptionallyCompose(e -> {
                    if (e.getCause() instanceof BridgeException &&
                        "CONFIRMATIONS_PENDING".equals(((BridgeException) e.getCause()).getCode())) {
                        return CompletableFuture.runAsync(() -> sleep(pollMs))
                                .thenCompose(v -> waitForLockProofInternal(id, pollMs, deadline));
                    }
                    return CompletableFuture.failedFuture(e);
                });
    }

    public CompletableFuture<SignedTransaction> mint(LockProof proof, String targetAddress, MintOptions options) {
        Map<String, Object> body = new HashMap<>();
        body.put("proof", proof);
        body.put("targetAddress", targetAddress);
        if (options != null) {
            if (options.getGasLimit() != null) body.put("gasLimit", options.getGasLimit());
            if (options.getMaxFeePerGas() != null) body.put("maxFeePerGas", options.getMaxFeePerGas());
        }
        return request("POST", "/transfers/mint", body)
                .thenApply(resp -> gson.fromJson(resp, SignedTransaction.class));
    }

    // ==================== Burn-Unlock Flow ====================

    public CompletableFuture<BurnReceipt> burn(String wrappedAsset, String amount, BurnOptions options) {
        Map<String, Object> body = new HashMap<>();
        body.put("wrappedAsset", wrappedAsset);
        body.put("amount", amount);
        if (options != null) {
            if (options.getRecipient() != null) body.put("recipient", options.getRecipient());
            if (options.getDeadline() != null) body.put("deadline", options.getDeadline());
        }
        return request("POST", "/transfers/burn", body)
                .thenApply(resp -> gson.fromJson(resp, BurnReceipt.class));
    }

    public CompletableFuture<BurnProof> getBurnProof(String burnReceiptId) {
        return request("GET", "/transfers/burn/" + encode(burnReceiptId) + "/proof", null)
                .thenApply(resp -> gson.fromJson(resp, BurnProof.class));
    }

    public CompletableFuture<BurnProof> waitForBurnProof(String burnReceiptId, long pollIntervalMs, long maxWaitMs) {
        long deadline = System.currentTimeMillis() + maxWaitMs;
        return waitForBurnProofInternal(burnReceiptId, pollIntervalMs, deadline);
    }

    private CompletableFuture<BurnProof> waitForBurnProofInternal(String id, long pollMs, long deadline) {
        if (System.currentTimeMillis() >= deadline) {
            return CompletableFuture.failedFuture(
                    new BridgeException("Timeout waiting for burn proof", "CONFIRMATIONS_PENDING", 0));
        }
        return getBurnProof(id)
                .exceptionallyCompose(e -> {
                    if (e.getCause() instanceof BridgeException &&
                        "CONFIRMATIONS_PENDING".equals(((BridgeException) e.getCause()).getCode())) {
                        return CompletableFuture.runAsync(() -> sleep(pollMs))
                                .thenCompose(v -> waitForBurnProofInternal(id, pollMs, deadline));
                    }
                    return CompletableFuture.failedFuture(e);
                });
    }

    public CompletableFuture<SignedTransaction> unlock(BurnProof proof, UnlockOptions options) {
        Map<String, Object> body = new HashMap<>();
        body.put("proof", proof);
        if (options != null) {
            if (options.getGasLimit() != null) body.put("gasLimit", options.getGasLimit());
            if (options.getGasPrice() != null) body.put("gasPrice", options.getGasPrice());
        }
        return request("POST", "/transfers/unlock", body)
                .thenApply(resp -> gson.fromJson(resp, SignedTransaction.class));
    }

    // ==================== Transfer Management ====================

    public CompletableFuture<Transfer> getTransfer(String transferId) {
        return request("GET", "/transfers/" + encode(transferId), null)
                .thenApply(resp -> gson.fromJson(resp, Transfer.class));
    }

    public CompletableFuture<TransferStatus> getTransferStatus(String transferId) {
        return getTransfer(transferId).thenApply(Transfer::getStatus);
    }

    public CompletableFuture<List<Transfer>> listTransfers(TransferFilter filter) {
        StringBuilder path = new StringBuilder("/transfers");
        if (filter != null) {
            StringBuilder params = new StringBuilder();
            if (filter.getStatus() != null) params.append("status=").append(filter.getStatus().name().toLowerCase());
            if (filter.getSourceChain() != null) {
                if (params.length() > 0) params.append("&");
                params.append("sourceChain=").append(filter.getSourceChain().name().toLowerCase());
            }
            if (filter.getTargetChain() != null) {
                if (params.length() > 0) params.append("&");
                params.append("targetChain=").append(filter.getTargetChain().name().toLowerCase());
            }
            if (filter.getLimit() != null) {
                if (params.length() > 0) params.append("&");
                params.append("limit=").append(filter.getLimit());
            }
            if (params.length() > 0) path.append("?").append(params);
        }
        return request("GET", path.toString(), null)
                .thenApply(resp -> {
                    Type type = new TypeToken<TransfersResponse>(){}.getType();
                    TransfersResponse result = gson.fromJson(resp, type);
                    return result.transfers;
                });
    }

    public CompletableFuture<Transfer> waitForTransfer(String transferId, long pollIntervalMs, long maxWaitMs) {
        long deadline = System.currentTimeMillis() + maxWaitMs;
        return waitForTransferInternal(transferId, pollIntervalMs, deadline);
    }

    private CompletableFuture<Transfer> waitForTransferInternal(String id, long pollMs, long deadline) {
        if (System.currentTimeMillis() >= deadline) {
            return CompletableFuture.failedFuture(new BridgeException("Timeout waiting for transfer completion"));
        }
        return getTransfer(id).thenCompose(transfer -> {
            if (FINAL_STATUSES.contains(transfer.getStatus())) {
                return CompletableFuture.completedFuture(transfer);
            }
            return CompletableFuture.runAsync(() -> sleep(pollMs))
                    .thenCompose(v -> waitForTransferInternal(id, pollMs, deadline));
        });
    }

    // ==================== Convenience Methods ====================

    public CompletableFuture<Transfer> bridgeTo(String asset, String amount, ChainId targetChain,
                                                 String targetAddress, LockOptions lockOpts, MintOptions mintOpts) {
        return lock(asset, amount, targetChain, lockOpts)
                .thenCompose(receipt -> waitForLockProof(receipt.getId(), 5000, 600000)
                        .thenCompose(proof -> mint(proof, targetAddress, mintOpts))
                        .thenCompose(tx -> waitForTransfer(receipt.getId(), 10000, 1800000)));
    }

    public CompletableFuture<Transfer> bridgeBack(String wrappedAsset, String amount,
                                                   BurnOptions burnOpts, UnlockOptions unlockOpts) {
        return burn(wrappedAsset, amount, burnOpts)
                .thenCompose(receipt -> waitForBurnProof(receipt.getId(), 5000, 600000)
                        .thenCompose(proof -> unlock(proof, unlockOpts))
                        .thenCompose(tx -> waitForTransfer(receipt.getId(), 10000, 1800000)));
    }

    // ==================== Lifecycle ====================

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

    public boolean isClosed() {
        return closed.get();
    }

    public CompletableFuture<Boolean> healthCheck() {
        return request("GET", "/health", null)
                .thenApply(resp -> {
                    Map<String, Object> map = gson.fromJson(resp, new TypeToken<Map<String, Object>>(){}.getType());
                    return "healthy".equals(map.get("status"));
                })
                .exceptionally(e -> false);
    }

    // ==================== Private Methods ====================

    private CompletableFuture<String> request(String method, String path, Object body) {
        if (closed.get()) {
            return CompletableFuture.failedFuture(new BridgeException("Client has been closed"));
        }
        return doRequest(method, path, body, 0);
    }

    private CompletableFuture<String> doRequest(String method, String path, Object body, int attempt) {
        String url = config.getEndpoint() + path;

        HttpRequest.Builder builder = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .header("Authorization", "Bearer " + config.getApiKey())
                .header("Content-Type", "application/json")
                .header("X-SDK-Version", "java/0.1.0")
                .timeout(Duration.ofSeconds(config.getTimeoutSecs()));

        HttpRequest.BodyPublisher bodyPub = body != null
                ? HttpRequest.BodyPublishers.ofString(gson.toJson(body))
                : HttpRequest.BodyPublishers.noBody();

        switch (method) {
            case "GET": builder.GET(); break;
            case "POST": builder.POST(bodyPub); break;
            case "PUT": builder.PUT(bodyPub); break;
            case "DELETE": builder.method("DELETE", bodyPub); break;
        }

        return httpClient.sendAsync(builder.build(), HttpResponse.BodyHandlers.ofString())
                .thenCompose(response -> {
                    if (response.statusCode() >= 400) {
                        Map<String, Object> errorBody = gson.fromJson(response.body(),
                                new TypeToken<Map<String, Object>>(){}.getType());
                        String message = errorBody != null && errorBody.get("message") != null
                                ? (String) errorBody.get("message") : "HTTP " + response.statusCode();
                        String code = errorBody != null ? (String) errorBody.get("code") : null;
                        return CompletableFuture.failedFuture(
                                new BridgeException(message, code, response.statusCode()));
                    }
                    return CompletableFuture.completedFuture(response.body());
                })
                .exceptionally(e -> {
                    if (attempt < config.getRetries() - 1) {
                        sleep((long) Math.pow(2, attempt) * 1000);
                        return doRequest(method, path, body, attempt + 1).join();
                    }
                    throw new RuntimeException(e);
                });
    }

    private static String encode(String value) {
        return java.net.URLEncoder.encode(value, java.nio.charset.StandardCharsets.UTF_8);
    }

    private static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }

    // Helper response types
    private static class ChainsResponse { List<Chain> chains; }
    private static class AssetsResponse { List<Asset> assets; }
    private static class TransfersResponse { List<Transfer> transfers; }
}
