/**
 * Copyright (c) 2013-2024 Nikita Koksharov
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.redisson.remote;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.util.CharsetUtil;
import io.netty.util.Timeout;
import io.netty.util.TimerTask;
import org.redisson.RedissonBlockingQueue;
import org.redisson.RedissonMap;
import org.redisson.api.RBlockingQueue;
import org.redisson.api.RFuture;
import org.redisson.api.RMap;
import org.redisson.api.RemoteInvocationOptions;
import org.redisson.api.annotation.RRemoteAsync;
import org.redisson.api.annotation.RRemoteReactive;
import org.redisson.api.annotation.RRemoteRx;
import org.redisson.client.codec.Codec;
import org.redisson.client.codec.StringCodec;
import org.redisson.codec.CompositeCodec;
import org.redisson.command.CommandAsyncExecutor;
import org.redisson.executor.RemotePromise;
import org.redisson.misc.Hash;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 
 * @author Nikita Koksharov
 *
 */
public abstract class BaseRemoteService {

    private final Map<Class<?>, String> requestQueueNameCache = new ConcurrentHashMap<>();
    private final ConcurrentMap<Method, long[]> methodSignaturesCache = new ConcurrentHashMap<>();

    protected final Codec codec;
    protected final String name;
    protected final CommandAsyncExecutor commandExecutor;
    protected final String executorId;

    protected final String cancelRequestMapName;
    protected final String cancelResponseMapName;
    protected final String responseQueueName;

    public BaseRemoteService(Codec codec, String name, CommandAsyncExecutor commandExecutor, String executorId) {
        this.codec = commandExecutor.getServiceManager().getCodec(codec);
        this.name = commandExecutor.getServiceManager().getConfig().getNameMapper().map(name);
        this.commandExecutor = commandExecutor;
        this.executorId = executorId;
        this.cancelRequestMapName = "{" + name + ":remote" + "}:cancel-request";
        this.cancelResponseMapName = "{" + name + ":remote" + "}:cancel-response";
        this.responseQueueName = getResponseQueueName(executorId);
    }

    public String getResponseQueueName(String executorId) {
        return "{remote_response}:" + executorId;
    }
    
    protected String getAckName(String requestId) {
        return "{" + name + ":remote" + "}:" + requestId + ":ack";
    }
    
    public String getRequestQueueName(Class<?> remoteInterface) {
        return requestQueueNameCache.computeIfAbsent(remoteInterface, k -> "{" + name + ":" + k.getName() + "}");
    }

    protected ByteBuf encode(Object obj) {
        try {
            return codec.getValueEncoder().encode(obj);
        } catch (IOException e) {
            throw new IllegalArgumentException(e);
        }
    }

    public <T> T get(Class<T> remoteInterface) {
        return get(remoteInterface, RemoteInvocationOptions.defaults());
    }

    public <T> T get(Class<T> remoteInterface, long executionTimeout, TimeUnit executionTimeUnit) {
        return get(remoteInterface,
                RemoteInvocationOptions.defaults().expectResultWithin(executionTimeout, executionTimeUnit));
    }

    public <T> T get(Class<T> remoteInterface, long executionTimeout, TimeUnit executionTimeUnit, long ackTimeout,
            TimeUnit ackTimeUnit) {
        return get(remoteInterface, RemoteInvocationOptions.defaults().expectAckWithin(ackTimeout, ackTimeUnit)
                .expectResultWithin(executionTimeout, executionTimeUnit));
    }

    public <T> T get(Class<T> remoteInterface, RemoteInvocationOptions options) {
        for (Annotation annotation : remoteInterface.getAnnotations()) {
            if (annotation.annotationType() == RRemoteAsync.class) {
                Class<T> syncInterface = (Class<T>) ((RRemoteAsync) annotation).value();
                AsyncRemoteProxy proxy = new AsyncRemoteProxy(commandExecutor, name, responseQueueName,
                        codec, executorId, cancelRequestMapName, this);
                return proxy.create(remoteInterface, options, syncInterface);
            }

            if (annotation.annotationType() == RRemoteReactive.class) {
                Class<T> syncInterface = (Class<T>) ((RRemoteReactive) annotation).value();
                ReactiveRemoteProxy proxy = new ReactiveRemoteProxy(commandExecutor, name, responseQueueName,
                        codec, executorId, cancelRequestMapName, this);
                return proxy.create(remoteInterface, options, syncInterface);
            }

            if (annotation.annotationType() == RRemoteRx.class) {
                Class<T> syncInterface = (Class<T>) ((RRemoteRx) annotation).value();
                RxRemoteProxy proxy = new RxRemoteProxy(commandExecutor, name, responseQueueName,
                        codec, executorId, cancelRequestMapName, this);
                return proxy.create(remoteInterface, options, syncInterface);
            }
        }

        SyncRemoteProxy proxy = new SyncRemoteProxy(commandExecutor, name, responseQueueName, codec, executorId, this);
        return proxy.create(remoteInterface, options);
    }

    protected long getTimeout(Long executionTimeoutInMillis, RemoteServiceRequest request) {
        return executionTimeoutInMillis;
    }

    protected <K, V> RMap<K, V> getMap(String name) {
        return new RedissonMap<>(new CompositeCodec(StringCodec.INSTANCE, codec, codec), commandExecutor, name);
    }
    
    protected <T> void scheduleCheck(String mapName, String requestId, CompletableFuture<T> cancelRequest) {
        commandExecutor.getServiceManager().newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                if (cancelRequest.isDone()) {
                    return;
                }

                RMap<String, T> canceledRequests = getMap(mapName);
                RFuture<T> future = canceledRequests.removeAsync(requestId);
                future.whenComplete((request, ex) -> {
                    if (cancelRequest.isDone()) {
                        return;
                    }
                    if (ex != null) {
                        scheduleCheck(mapName, requestId, cancelRequest);
                        return;
                    }
                    
                    if (request == null) {
                        scheduleCheck(mapName, requestId, cancelRequest);
                    } else {
                        cancelRequest.complete(request);
                    }
                });
            }
        }, 3000, TimeUnit.MILLISECONDS);
    }

    protected String generateRequestId(Object[] args) {
        return commandExecutor.getServiceManager().generateId();
    }

    protected abstract CompletableFuture<Boolean> addAsync(String requestQueueName, RemoteServiceRequest request,
                                                           RemotePromise<Object> result);

    protected abstract CompletableFuture<Boolean> removeAsync(String requestQueueName, String taskId);

    protected long[] getMethodSignature(Method method) {
        long[] result = methodSignaturesCache.get(method);
        if (result == null) {
            String str = Arrays.stream(method.getParameterTypes())
                                .map(c -> c.getName())
                                .collect(Collectors.joining());
            ByteBuf buf = Unpooled.copiedBuffer(str, CharsetUtil.UTF_8);
            result = Hash.hash128(buf);
            buf.release();
            long[] oldResult = methodSignaturesCache.putIfAbsent(method, result);
            if (oldResult != null) {
                return oldResult;
            }
        }
        
        return result;
    }

    protected <V> RBlockingQueue<V> getBlockingQueue(String name, Codec codec) {
        return new RedissonBlockingQueue<>(codec, commandExecutor, name);
    }

}
