package com.sonic.common.client.impl;

import com.alibaba.fastjson.JSON;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.common.hash.Hashing;
import com.google.common.io.BaseEncoding;
import com.sonic.common.AppRuntime;
import com.sonic.common.auth.AuthCaller;
import com.sonic.common.client.AppCenterClient;
import com.sonic.common.client.TimerRefreshCache;
import com.sonic.common.enums.AppEnv;
import com.sonic.common.exception.BizException;
import com.sonic.common.rpc.GlobalResultCode;
import com.sonic.common.rpc.RpcClient;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.util.CollectionUtils;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author chenjun
 */
public class AppCenterClientImpl implements AppCenterClient {

    /** load cache immediately */
    private static final long INITIAL_DELAY_MILLIS = 0;
    private static final long REFRESH_INTERVAL_MILLIS = TimeUnit.MINUTES.toMillis(10);

    private static final String TOKEN_PATH = "/app/token";
    private static final String LIST = "/app/list";

    private String host;
    private String secret;
    private RpcClient rpcClient;
    private AppRuntime appRuntime;
    private BiConsumer<String, Object[]> alertConsumer;
    /**
     * 支持外部提供app列表，当appCenter返回的app与外部提供的app有相同的appId或serviceName时，优先使用外部提供的app
     */
    private Supplier<List<App>> appSupplier;

    private TimerRefreshCache<Map<String, App>> appCache;

    @Override
    public String token() {
        LocalDateTime now = LocalDateTime.now();
        TokenReq req = TokenReq.builder()
                .appId(appRuntime.getAppId()).appEnv(appRuntime.getEnv()).timestamp(now)
                .signature(buildSignature(secret, null, now)).build();
        return rpcClient.request()
                .url(host + TOKEN_PATH)
                .content(req)
                .clz(String.class)
                .post();
    }

    @Override
    public List<App> list(ListReq listReq) {
        List<App> apps = rpcClient.request()
                .url(host + LIST)
                .content(listReq)
                .clz(App.class)
                .postAndGetList();
        if (appSupplier == null) {
            return apps;
        }
        // 如果有指定的supplier, 使用指定的supplier获取的app替换app center返回的app
        List<App> suppliedApps = appSupplier.get().stream()
                .filter(a -> !Strings.isNullOrEmpty(a.getId())
                        && !Strings.isNullOrEmpty(a.getServiceName())
                        && !Strings.isNullOrEmpty(a.getHost()))
                .collect(Collectors.toList());
        if (CollectionUtils.isEmpty(suppliedApps)) {
            return apps;
        }

        Set<String> suppliedAppIds = suppliedApps.stream().map(App::getId)
                .filter(Objects::nonNull).collect(Collectors.toSet());
        Set<String> suppliedServiceNames = suppliedApps.stream().map(App::getServiceName)
                .filter(Objects::nonNull).collect(Collectors.toSet());
        // 过滤appCenter返回的相同appId或serviceName的app，并添加外部提供的app列表，便于在本地测试时
        // 可以使用本地的配置替换appCenter中的配置
        return Stream.concat(apps.stream().filter(app -> !suppliedAppIds.contains(app.getId())
                && !suppliedServiceNames.contains(app.getServiceName())), suppliedApps.stream())
                .collect(Collectors.toList());
    }

    @Override
    public Optional<App> getById(String appId) {
        return Optional.ofNullable(appCache.get()).map(m -> m.get(appId));
    }

    @Override
    public String getNameByAppId(String appId) {
        return getById(appId)
                .map(App::getServiceName)
                .orElseThrow(() -> new BizException(
                        GlobalResultCode.INVALID_PARAMS.getErrorCode(), String.format("指定的appId尚未在AppCenter注册, appId = %s", appId)));
    }

    private Map<String, App> loadAllAppHosts() {
        ListReq req = ListReq.builder().appEnv(appRuntime.getEnv()).build();
        try {
            // supplier提供的App可能没有id，需要过滤
            List<App> list = list(req).stream().filter(a -> !Strings.isNullOrEmpty(a.getId())).collect(Collectors.toList());
            return Maps.uniqueIndex(list, App::getId);
        } catch (Exception e) {
            postAlert("AppCenterClient拉取app列表失败, req = {}", req, e);
            throw e;
        }
    }

    private void postAlert(String message, Object... params) {
        if (alertConsumer != null) {
            alertConsumer.accept(message, params);
        }
    }

    private AppCenterClientImpl(Builder builder) {
        this.rpcClient = builder.getRpcClient();
        this.host = builder.getHost();
        this.appRuntime = builder.getAppRuntime();
        this.secret = builder.getSecret();
        this.alertConsumer = builder.getAlertConsumer();
        this.appSupplier = builder.getAppSupplier();
        this.appCache = new TimerRefreshCache<>("appCache", INITIAL_DELAY_MILLIS,
                REFRESH_INTERVAL_MILLIS, builder.getExecutor(), this::loadAllAppHosts);

        Preconditions.checkArgument(!Strings.isNullOrEmpty(appRuntime.getAppId()), "缺少appId");
        Preconditions.checkArgument(appRuntime.getEnv() != null, "缺少appEnv");
        Preconditions.checkArgument(rpcClient != null, "缺少 rpcClient");
        Preconditions.checkArgument(!Strings.isNullOrEmpty(secret), "缺少 secret");
    }

    public static Builder builder() {
        return new Builder();
    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Builder {
        private String secret;
        @Deprecated
        private String host = "";
        private AppRuntime appRuntime;
        private RpcClient rpcClient;
        private ScheduledThreadPoolExecutor executor;
        private BiConsumer<String, Object[]> alertConsumer;
        private Supplier<List<App>> appSupplier;

        public Builder secret(String secret) {
            this.secret = secret;
            return this;
        }

        public Builder host(String host) {
            this.host = Strings.nullToEmpty(host);
            return this;
        }

        public Builder appRuntime(AppRuntime appRuntime) {
            this.appRuntime = appRuntime;
            return this;
        }

        public Builder rpcClient(RpcClient rpcClient) {
            this.rpcClient = rpcClient;
            return this;
        }

        public Builder executor(ScheduledThreadPoolExecutor executor) {
            this.executor = executor;
            return this;
        }

        public Builder alertConsumer(BiConsumer<String, Object[]> alertConsumer) {
            this.alertConsumer = alertConsumer;
            return this;
        }

        public Builder appSupplier(Supplier<List<App>> appSupplier) {
            this.appSupplier = appSupplier;
            return this;
        }

        public AppCenterClient build() {
            Preconditions.checkArgument(executor != null, "executor不能为空");
            Preconditions.checkArgument(appRuntime != null, "appRuntime不能为空");
            Preconditions.checkArgument(!Strings.isNullOrEmpty(appRuntime.getAppId()), "缺少appId");
            Preconditions.checkArgument(appRuntime.getEnv() != null, "缺少appEnv");
            Preconditions.checkArgument(rpcClient != null, "缺少 rpcClient");
            Preconditions.checkArgument(!Strings.isNullOrEmpty(secret), "缺少 secret");

            if (appRuntime.getEnv() == AppEnv.unittest) {
                return buildDummyAppCenterClient();
            } else {
                return new AppCenterClientImpl(this);
            }
        }

        private AppCenterClientImpl buildDummyAppCenterClient() {
            // build dummy implement for app center client
            return new AppCenterClientImpl(this) {
                @Override
                public String token() {
                    return JSON.toJSONString(AuthCaller.builder()
                            .appId(appRuntime.getAppId()).appEnv(appRuntime.getEnv()).build());
                }

                @Override
                public List<App> list(ListReq listReq) {
                    return ImmutableList.of(App.builder().id("0001").displayName("测试")
                            .serviceName("test").status(Status.ENABLED)
                            .dept("bs")
                            .host("http://localhost:8080").build());
                }

                @Override
                public String getNameByAppId(String appId) {
                    return "test";
                }
            };
        }
    }

    @lombok.Builder
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    static class TokenReq {
        private String appId;

        private LocalDateTime timestamp;

        private String signature;
        private String ext;
        private AppEnv appEnv;
    }

    private String buildSignature(String secret, String ext, LocalDateTime timestamp) {
        return BaseEncoding.base64().encode(Hashing.sha256().newHasher()
                .putString(appRuntime.getAppId() + appRuntime.getEnv().name()
                        + ext + timestamp.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() + secret, Charsets.UTF_8)
                .hash()
                .asBytes());
    }

}
