package com.qmth.boot.core.retrofit.core;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.qmth.boot.core.retrofit.annotatioin.RetrofitClient;
import com.qmth.boot.core.retrofit.config.RetrofitProperties;
import com.qmth.boot.core.retrofit.interceptor.ErrorDecodeInterceptor;
import com.qmth.boot.core.retrofit.interceptor.LoggingInterceptor;
import com.qmth.boot.core.retrofit.interceptor.RetryInterceptor;
import com.qmth.boot.core.retrofit.interceptor.SignatureInterceptor;
import com.qmth.boot.core.retrofit.interfaces.CustomizeRetrofitConfiguration;
import com.qmth.boot.tools.syntax.Optional;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.binder.okhttp3.OkHttpMetricsEventListener;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.boot.logging.LogLevel;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import retrofit2.Retrofit;
import retrofit2.converter.jackson.JacksonConverterFactory;
import retrofit2.converter.scalars.ScalarsConverterFactory;

import java.lang.reflect.Proxy;

public class RetrofitFactoryBean<T> implements FactoryBean<T>, ApplicationContextAware {

    private ApplicationContext applicationContext;

    private Class<T> retrofitInterface;

    private RetrofitProperties retrofitProperties;

    private MeterRegistry meterRegistry;

    private ObjectMapper mapper;

    public RetrofitFactoryBean(Class<T> retrofitInterface) {
        this.retrofitInterface = retrofitInterface;
        // 初始化ObjectMapper
        ObjectMapper mapper = new ObjectMapper();
        // 反序列化时，允许有未知字段
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        // 默认不序列化空值
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 解决jackson2无法反序列化LocalDateTime的问题
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        mapper.registerModule(new JavaTimeModule());
        this.mapper = mapper;
    }

    @Override
    public T getObject() {
        Retrofit retrofit = getRetrofit(retrofitInterface);
        // source
        T source = retrofit.create(retrofitInterface);
        // proxy
        return (T) Proxy.newProxyInstance(retrofitInterface.getClassLoader(), new Class<?>[] { retrofitInterface },
                new RetrofitInvocationHandler(source, retrofitProperties));
    }

    @Override
    public Class<T> getObjectType() {
        return this.retrofitInterface;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }

    private synchronized Retrofit getRetrofit(Class<?> retrofitClientInterfaceClass) {
        RetrofitClient retrofitClient = retrofitClientInterfaceClass.getAnnotation(RetrofitClient.class);
        CustomizeRetrofitConfiguration customize = applicationContext.getBean(retrofitClient.configuration());

        OkHttpClient client = getOkHttpClient(retrofitClient, customize);
        Retrofit.Builder retrofitBuilder = new Retrofit.Builder().baseUrl(getBaseUrl(retrofitClient, customize))
                .addConverterFactory(ScalarsConverterFactory.create())
                .addConverterFactory(JacksonConverterFactory.create(mapper)).validateEagerly(true).client(client);
        if (retrofitClient.directReturn()) {
            retrofitBuilder.addCallAdapterFactory(new DirectCallAdapterFactory());
        }
        return retrofitBuilder.build();
    }

    /**
     * Get OkHttpClient instance, one interface corresponds to one OkHttpClient
     *
     * @param retrofitClient @RetrofitClient
     * @return OkHttpClient instance
     */
    private OkHttpClient getOkHttpClient(RetrofitClient retrofitClient, CustomizeRetrofitConfiguration customize) {
        OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder().connectionPool(new ConnectionPool())
                .connectTimeout(
                        Optional.firstNotNull(customize.getConnectTimeout(), retrofitProperties.getConnectTimeout()))
                .readTimeout(Optional.firstNotNull(customize.getReadTimeout(), retrofitProperties.getReadTimeout()))
                .writeTimeout(Optional.firstNotNull(customize.getWriteTimeout(), retrofitProperties.getWriteTimeout()));

        // add signature interceptor
        okHttpClientBuilder.addInterceptor(new SignatureInterceptor(customize.getSignature()));

        // add ErrorDecodeInterceptor
        okHttpClientBuilder.addInterceptor(new ErrorDecodeInterceptor());

        // add retry interceptor
        okHttpClientBuilder.addInterceptor(new RetryInterceptor(
                Optional.firstNotNull(customize.getRetryProperties(), retrofitProperties.getRetry())));

        // add logging interceptor
        LogLevel logLevel = getLogLevel(customize, retrofitClient);
        if (!logLevel.equals(LogLevel.OFF)) {
            okHttpClientBuilder.addNetworkInterceptor(new LoggingInterceptor(logLevel));
        }

        // add metrics event listener
        if (meterRegistry != null) {
            okHttpClientBuilder
                    .eventListener(OkHttpMetricsEventListener.builder(meterRegistry, "okhttp.client").build());
        }

        return okHttpClientBuilder.build();
    }

    private String getBaseUrl(RetrofitClient retrofitClient, CustomizeRetrofitConfiguration customize) {
        String url = customize.getBaseUrl();
        if (StringUtils.isBlank(url)) {
            url = retrofitClient.baseUrl();
        }
        return url;
    }

    private LogLevel getLogLevel(CustomizeRetrofitConfiguration customize, RetrofitClient retrofitClient) {
        LogLevel level = retrofitClient.logLevel();
        if (level.equals(LogLevel.OFF)) {
            level = Optional.firstNotNull(customize.getLogLevel(), retrofitProperties.getLogLevel());
        }
        return level;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
        this.retrofitProperties = applicationContext.getBean(RetrofitProperties.class);
        this.meterRegistry = applicationContext.getBean(MeterRegistry.class);
    }

}
