package io.synor.contract;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
import okhttp3.*;

import java.io.IOException;
import java.lang.reflect.Type;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Synor Contract SDK client for Java.
 * Smart contract deployment, interaction, and event handling.
 */
public class ContractClient implements AutoCloseable {
    private static final String SDK_VERSION = "0.1.0";
    private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");

    private final ContractConfig config;
    private final OkHttpClient httpClient;
    private final Gson gson;
    private final AtomicBoolean closed = new AtomicBoolean(false);

    public ContractClient(ContractConfig config) {
        this.config = config;
        this.gson = new GsonBuilder().create();
        this.httpClient = new OkHttpClient.Builder()
                .connectTimeout(config.getTimeoutMs(), TimeUnit.MILLISECONDS)
                .readTimeout(config.getTimeoutMs(), TimeUnit.MILLISECONDS)
                .writeTimeout(config.getTimeoutMs(), TimeUnit.MILLISECONDS)
                .build();
    }

    // ==================== Contract Deployment ====================

    public CompletableFuture<DeploymentResult> deploy(DeployContractOptions options) {
        JsonObject body = new JsonObject();
        body.addProperty("bytecode", options.bytecode);
        if (options.abi != null) body.add("abi", gson.toJsonTree(options.abi));
        if (options.constructorArgs != null) body.add("constructor_args", gson.toJsonTree(options.constructorArgs));
        if (options.value != null) body.addProperty("value", options.value);
        if (options.gasLimit != null) body.addProperty("gas_limit", options.gasLimit);
        if (options.gasPrice != null) body.addProperty("gas_price", options.gasPrice);
        if (options.nonce != null) body.addProperty("nonce", options.nonce);

        return post("/contract/deploy", body, DeploymentResult.class);
    }

    public CompletableFuture<DeploymentResult> deployCreate2(DeployContractOptions options, String salt) {
        JsonObject body = new JsonObject();
        body.addProperty("bytecode", options.bytecode);
        body.addProperty("salt", salt);
        if (options.abi != null) body.add("abi", gson.toJsonTree(options.abi));
        if (options.constructorArgs != null) body.add("constructor_args", gson.toJsonTree(options.constructorArgs));
        if (options.value != null) body.addProperty("value", options.value);
        if (options.gasLimit != null) body.addProperty("gas_limit", options.gasLimit);
        if (options.gasPrice != null) body.addProperty("gas_price", options.gasPrice);

        return post("/contract/deploy/create2", body, DeploymentResult.class);
    }

    public CompletableFuture<String> predictAddress(String bytecode, String salt, String deployer) {
        JsonObject body = new JsonObject();
        body.addProperty("bytecode", bytecode);
        body.addProperty("salt", salt);
        if (deployer != null) body.addProperty("deployer", deployer);

        return post("/contract/predict-address", body, JsonObject.class)
                .thenApply(response -> response.get("address").getAsString());
    }

    // ==================== Contract Interaction ====================

    public CompletableFuture<Object> call(CallContractOptions options) {
        JsonObject body = new JsonObject();
        body.addProperty("contract", options.contract);
        body.addProperty("method", options.method);
        body.add("args", gson.toJsonTree(options.args));
        body.add("abi", gson.toJsonTree(options.abi));

        return post("/contract/call", body, Object.class);
    }

    public CompletableFuture<TransactionResult> send(SendContractOptions options) {
        JsonObject body = new JsonObject();
        body.addProperty("contract", options.contract);
        body.addProperty("method", options.method);
        body.add("args", gson.toJsonTree(options.args));
        body.add("abi", gson.toJsonTree(options.abi));
        if (options.value != null) body.addProperty("value", options.value);
        if (options.gasLimit != null) body.addProperty("gas_limit", options.gasLimit);
        if (options.gasPrice != null) body.addProperty("gas_price", options.gasPrice);
        if (options.nonce != null) body.addProperty("nonce", options.nonce);

        return post("/contract/send", body, TransactionResult.class);
    }

    // ==================== Events ====================

    public CompletableFuture<List<DecodedEvent>> getEvents(EventFilter filter) {
        JsonObject body = new JsonObject();
        body.addProperty("contract", filter.contract);
        if (filter.event != null) body.addProperty("event", filter.event);
        if (filter.fromBlock != null) body.addProperty("from_block", filter.fromBlock);
        if (filter.toBlock != null) body.addProperty("to_block", filter.toBlock);
        if (filter.topics != null) body.add("topics", gson.toJsonTree(filter.topics));
        if (filter.abi != null) body.add("abi", gson.toJsonTree(filter.abi));

        Type listType = new TypeToken<List<DecodedEvent>>(){}.getType();
        return post("/contract/events", body, listType);
    }

    public CompletableFuture<List<EventLog>> getLogs(String contract, Long fromBlock, Long toBlock) {
        StringBuilder path = new StringBuilder("/contract/logs?contract=").append(encode(contract));
        if (fromBlock != null) path.append("&from_block=").append(fromBlock);
        if (toBlock != null) path.append("&to_block=").append(toBlock);

        Type listType = new TypeToken<List<EventLog>>(){}.getType();
        return get(path.toString(), listType);
    }

    public CompletableFuture<List<DecodedEvent>> decodeLogs(List<EventLog> logs, List<AbiEntry> abi) {
        JsonObject body = new JsonObject();
        body.add("logs", gson.toJsonTree(logs));
        body.add("abi", gson.toJsonTree(abi));

        Type listType = new TypeToken<List<DecodedEvent>>(){}.getType();
        return post("/contract/decode-logs", body, listType);
    }

    // ==================== ABI Utilities ====================

    public CompletableFuture<String> encodeCall(EncodeCallOptions options) {
        JsonObject body = new JsonObject();
        body.addProperty("method", options.method);
        body.add("args", gson.toJsonTree(options.args));
        body.add("abi", gson.toJsonTree(options.abi));

        return post("/contract/encode", body, JsonObject.class)
                .thenApply(response -> response.get("data").getAsString());
    }

    public CompletableFuture<Object> decodeResult(DecodeResultOptions options) {
        JsonObject body = new JsonObject();
        body.addProperty("data", options.data);
        body.addProperty("method", options.method);
        body.add("abi", gson.toJsonTree(options.abi));

        return post("/contract/decode", body, JsonObject.class)
                .thenApply(response -> response.get("result"));
    }

    public CompletableFuture<String> getSelector(String signature) {
        return get("/contract/selector?signature=" + encode(signature), JsonObject.class)
                .thenApply(response -> response.get("selector").getAsString());
    }

    // ==================== Gas Estimation ====================

    public CompletableFuture<GasEstimation> estimateGas(EstimateGasOptions options) {
        JsonObject body = new JsonObject();
        body.addProperty("contract", options.contract);
        body.addProperty("method", options.method);
        body.add("args", gson.toJsonTree(options.args));
        body.add("abi", gson.toJsonTree(options.abi));
        if (options.value != null) body.addProperty("value", options.value);

        return post("/contract/estimate-gas", body, GasEstimation.class);
    }

    // ==================== Contract Information ====================

    public CompletableFuture<BytecodeInfo> getBytecode(String address) {
        return get("/contract/" + encode(address) + "/bytecode", BytecodeInfo.class);
    }

    public CompletableFuture<VerificationResult> verify(VerifyContractOptions options) {
        JsonObject body = new JsonObject();
        body.addProperty("address", options.address);
        body.addProperty("source_code", options.sourceCode);
        body.addProperty("compiler_version", options.compilerVersion);
        if (options.constructorArgs != null) body.addProperty("constructor_args", options.constructorArgs);
        if (options.optimization != null) body.addProperty("optimization", options.optimization);
        if (options.optimizationRuns != null) body.addProperty("optimization_runs", options.optimizationRuns);
        if (options.license != null) body.addProperty("license", options.license);

        return post("/contract/verify", body, VerificationResult.class);
    }

    public CompletableFuture<VerificationResult> getVerificationStatus(String address) {
        return get("/contract/" + encode(address) + "/verification", VerificationResult.class);
    }

    // ==================== Multicall ====================

    public CompletableFuture<List<MulticallResult>> multicall(List<MulticallRequest> requests) {
        JsonObject body = new JsonObject();
        body.add("calls", gson.toJsonTree(requests));

        Type listType = new TypeToken<List<MulticallResult>>(){}.getType();
        return post("/contract/multicall", body, listType);
    }

    // ==================== Storage ====================

    public CompletableFuture<String> readStorage(ReadStorageOptions options) {
        StringBuilder path = new StringBuilder("/contract/storage?contract=")
                .append(encode(options.contract))
                .append("&slot=").append(encode(options.slot));
        if (options.blockNumber != null) path.append("&block=").append(options.blockNumber);

        return get(path.toString(), JsonObject.class)
                .thenApply(response -> response.get("value").getAsString());
    }

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

    public CompletableFuture<Boolean> healthCheck() {
        if (closed.get()) {
            return CompletableFuture.completedFuture(false);
        }
        return get("/health", JsonObject.class)
                .thenApply(response -> "healthy".equals(response.get("status").getAsString()))
                .exceptionally(e -> false);
    }

    @Override
    public void close() {
        closed.set(true);
        httpClient.dispatcher().executorService().shutdown();
        httpClient.connectionPool().evictAll();
    }

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

    // ==================== HTTP Helpers ====================

    private String encode(String value) {
        return URLEncoder.encode(value, StandardCharsets.UTF_8);
    }

    private <T> CompletableFuture<T> get(String path, Type type) {
        return executeRequest(path, null, type);
    }

    private <T> CompletableFuture<T> post(String path, JsonObject body, Type type) {
        return executeRequest(path, body, type);
    }

    private <T> CompletableFuture<T> executeRequest(String path, JsonObject body, Type type) {
        if (closed.get()) {
            CompletableFuture<T> future = new CompletableFuture<>();
            future.completeExceptionally(new ContractException("Client has been closed"));
            return future;
        }

        CompletableFuture<T> future = new CompletableFuture<>();

        Request.Builder requestBuilder = new Request.Builder()
                .url(config.getEndpoint() + path)
                .header("Authorization", "Bearer " + config.getApiKey())
                .header("Content-Type", "application/json")
                .header("X-SDK-Version", "java/" + SDK_VERSION);

        if (body != null) {
            requestBuilder.post(RequestBody.create(gson.toJson(body), JSON));
        } else {
            requestBuilder.get();
        }

        executeWithRetry(requestBuilder.build(), config.getRetries(), 0, future, type);
        return future;
    }

    private <T> void executeWithRetry(Request request, int remainingRetries, int attempt, CompletableFuture<T> future, Type type) {
        httpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                if (remainingRetries > 1) {
                    scheduleRetry(request, remainingRetries - 1, attempt + 1, future, type);
                } else {
                    future.completeExceptionally(new ContractException("Request failed: " + e.getMessage(), e));
                }
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                try (ResponseBody responseBody = response.body()) {
                    String bodyString = responseBody != null ? responseBody.string() : "";

                    if (response.isSuccessful()) {
                        T result = gson.fromJson(bodyString, type);
                        future.complete(result);
                    } else {
                        JsonObject errorObj = gson.fromJson(bodyString, JsonObject.class);
                        String message = errorObj.has("message") ? errorObj.get("message").getAsString() : "Unknown error";
                        String code = errorObj.has("code") ? errorObj.get("code").getAsString() : null;

                        if (remainingRetries > 1 && response.code() >= 500) {
                            scheduleRetry(request, remainingRetries - 1, attempt + 1, future, type);
                        } else {
                            future.completeExceptionally(new ContractException(message, response.code(), code));
                        }
                    }
                }
            }
        });
    }

    private <T> void scheduleRetry(Request request, int remainingRetries, int attempt, CompletableFuture<T> future, Type type) {
        long delay = (long) Math.pow(2, attempt) * 1000;
        CompletableFuture.delayedExecutor(delay, TimeUnit.MILLISECONDS)
                .execute(() -> executeWithRetry(request, remainingRetries, attempt, future, type));
    }
}
