package com.spring.infra.feign.config;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import org.springframework.cloud.openfeign.FeignFormatterRegistrar;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar;
import org.springframework.http.MediaType;

import feign.Client;
import feign.Logger;
import feign.Request;
import feign.RequestInterceptor;
import feign.Response;
import feign.RetryableException;
import feign.Retryer;
import feign.codec.ErrorDecoder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.RequestBody;

/**
 * Feign 클라이언트에 대한 공통 설정을 제공하는 설정 클래스입니다.
 * 이 클래스는 기본 Feign 빌더 설정과 클라이언트별 커스텀 설정을 제공합니다.
 * 
 * @author mindol
 * @version 1.0
 */
@Slf4j
@Configuration
@RequiredArgsConstructor
public class CommonFeignConfig {

    private final FeignClientProperties properties;

    @Bean
    Client feignClient() {
        return new Client() {
            @Override
            public Response execute(Request request, Request.Options options) throws IOException {
                String clientName = request.requestTemplate().feignTarget().name();
                FeignClientProperties.ClientConfig config = properties.getClients().get(clientName);

                OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder()
                        .connectTimeout(config.getConnectTimeout(), TimeUnit.MILLISECONDS)
                        .readTimeout(config.getReadTimeout(), TimeUnit.MILLISECONDS);

                if (config.isUseProxy()) {
                    clientBuilder.proxy(new Proxy(
                            Proxy.Type.HTTP,
                            new InetSocketAddress(config.getProxyHost(), config.getProxyPort())
                    ));
                }

                OkHttpClient okHttpClient = clientBuilder.build();
                // OkHttp 요청 생성
                okhttp3.Request.Builder okHttpRequestBuilder = new okhttp3.Request.Builder()
                    .url(request.url())
                    .method(request.httpMethod().name(), request.httpMethod() == Request.HttpMethod.GET ? null : getRequestBody(request)); // 요청 본문 설정

                // Feign 요청의 헤더를 OkHttp 요청에 추가
                request.headers().forEach((key, values) -> {
                    for (String value : values) {
                        okHttpRequestBuilder.addHeader(key, value);
                    }
                });

                okhttp3.Request okHttpRequest = okHttpRequestBuilder.build();

                try (okhttp3.Response okHttpResponse = okHttpClient.newCall(okHttpRequest).execute()) {
                    return Response.builder()
                            .status(okHttpResponse.code())
                            .reason(okHttpResponse.message())
                            .headers(convertHeaders(okHttpResponse.headers()))
                            .body(okHttpResponse.body().bytes())
                            .request(request)
                            .build();
                }
            }
        };
    }

    private Map<String, Collection<String>> convertHeaders(okhttp3.Headers headers) {
        Map<String, Collection<String>> convertedHeaders = new HashMap<>();
        for (String name : headers.names()) {
            convertedHeaders.put(name, headers.values(name));
        }
        return convertedHeaders;
    }

    private RequestBody getRequestBody(Request request) {
        String contentType = MediaType.APPLICATION_JSON_VALUE;
    
        if (request.requestTemplate().headers().containsKey("Content-Type")) {
            contentType = request.requestTemplate().headers().get("Content-Type").iterator().next();
        }
    
        if (request.body() != null) {
            return RequestBody.create(request.body(), okhttp3.MediaType.parse(contentType));
        } else {
            return RequestBody.create("", okhttp3.MediaType.parse(contentType));
        }
    }

    @Bean
    RequestInterceptor headerInterceptor() {
        return requestTemplate -> {
            String clientName = requestTemplate.feignTarget().name();
            if (properties.getClients().containsKey(clientName)) {
                var config = properties.getClients().get(clientName);
                if (!config.getHeaders().isEmpty()) {
                    config.getHeaders().forEach(requestTemplate::header);
                }
            }
        };
    }

    @Bean
    Retryer retryer() {
        // 0.1초의 간격으로 시작해 최대 3초의 간격으로 점점 증가하며, 최대5번 재시도한다.
        return new Retryer.Default(100L, TimeUnit.SECONDS.toMillis(3L), 5);
    }

    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }

    @Bean
    FeignFormatterRegistrar dateTimeFormatterRegistrar() {
        return registry -> {
            DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
            registrar.setUseIsoFormat(true);
            registrar.registerFormatters(registry);
        };
    }

    @Bean
    ErrorDecoder errorDecoder() {
        return (methodKey, response) -> 
            (response.status() >= 500 && response.status() <= 504) 
                ? new RetryableException(
                    response.status(),
                    String.format("Server error %d: %s, retrying...", response.status(), response.reason()),
                    response.request().httpMethod(),
                    null,
                    response.request()
                ) 
                : new Exception(String.format("Error %d: %s", response.status(), response.reason()));
    }

}
