package com.ffii.fpsms.api.service

import com.ffii.core.utils.JwtTokenUtil
import com.ffii.fpsms.m18.M18Config
import com.ffii.fpsms.m18.service.M18TokenService
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
import org.springframework.stereotype.Service
import org.springframework.util.LinkedMultiValueMap
import org.springframework.util.MultiValueMap
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.bodyToMono
import reactor.core.publisher.Mono
import kotlin.reflect.full.memberProperties
import org.springframework.cloud.context.config.annotation.RefreshScope
import org.springframework.context.annotation.Lazy
import org.springframework.http.HttpStatus
import org.springframework.http.HttpStatusCode
import org.springframework.web.reactive.function.client.ClientRequest
import org.springframework.web.reactive.function.client.WebClientResponseException
import org.springframework.web.reactive.function.client.awaitBody

@Service
open class ApiCallerService(
    val m18Config: M18Config,
    @Lazy val m18TokenService: M18TokenService,
) {
    val logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java)

    val webClient: WebClient = WebClient.builder()
        .baseUrl(m18Config.BASE_URL)
        .defaultHeaders { headers ->
            headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
//            headers.set(HttpHeaders.AUTHORIZATION, "Bearer ${m18Config.ACCESS_TOKEN}")
            headers.set("client_id", m18Config.CLIENT_ID)
        }
        .filter { request, next ->
            // Dynamically fetch the latest ACCESS_TOKEN
            val updatedRequest = ClientRequest.from(request)
                .header(HttpHeaders.AUTHORIZATION, "Bearer ${m18Config.ACCESS_TOKEN}")
                .build()
            next.exchange(updatedRequest)
        }
        .codecs { configurer -> configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024) }
        .build()

    /**
     * Performs a GET HTTP request to the specified URL path.
     *
     * @param urlPath The path to send the GET request to (after /jsf/rfws)
     * @param params Optional query parameters to include in the request
     * @param customHeaders Optional custom headers to include in the request. The default header includes CONTENT_TYPE, AUTHORIZATION, client_id
     * @return A Mono that emits the response body converted to type T
     */
    inline fun <reified T : Any> get(
        urlPath: String,
        params: MultiValueMap<String, String>,
        customHeaders: Map<String, String>?
    ): Mono<T> {
        println("ACCESS TOKEN: ${m18Config.ACCESS_TOKEN}")
        return webClient.get()
            .uri { uriBuilder ->
                uriBuilder
                    .apply {
                        path(urlPath)

                        // convert to multiValueMap
                        queryParams(params)
                    }
                    .build()
            }
            .headers { headers ->
                customHeaders?.forEach { (k, v) -> headers.set(k, v) }
            }
            .retrieve()
            .bodyToMono(T::class.java)
            .doOnError { error ->
                println("Error occurred: ${error.message}")
            }
            .onErrorResume(WebClientResponseException::class.java) { error ->
                logger.error("WebClientResponseException")
                logger.error("Error Status: ${error.statusCode} - ${error.statusText}")
                logger.error("Error Message: ${error.message}")
                if (error.statusCode == HttpStatusCode.valueOf(400)) {
                    m18TokenService.run()
                }
                Mono.error(error)
            }
            .onErrorResume { error ->
                logger.error("Exception")
                logger.error("Error Message: ${error.message}")
                Mono.error(error)
            }
            .retry(5)
    }

    /**
     * Performs a GET HTTP request to the specified URL path.
     *
     * @param urlPath The path to send the GET request to (after /jsf/rfws)
     * @param params Optional query parameters to include in the request
     * @param customHeaders Optional custom headers to include in the request. The default header includes CONTENT_TYPE, AUTHORIZATION, client_id
     * @return A Mono that emits the response body converted to type T
     */
    inline fun <reified T : Any> get(
        urlPath: String,
        params: Map<String, Any>?,
        customHeaders: Map<String, String>?
    ): Mono<T> {

        // convert to multiValueMap<String, String>
        val queryParams = params?.let { paramMap ->
            LinkedMultiValueMap<String, String>().apply {
                paramMap.forEach { (key, value) ->
                    when (value) {
                        is Collection<*> -> addAll(key, value.map { it.toString() })
                        else -> add(key, value.toString())
                    }
                }
            }
        } ?: LinkedMultiValueMap<String, String>()

        return get<T>(urlPath, queryParams, customHeaders)
    }

    /**
     * Performs a GET HTTP request to the specified URL path.
     *
     * @param urlPath The path to send the GET request to (after /jsf/rfws)
     * @param params Optional query parameters to include in the request
     * @return A Mono that emits the response body converted to type T
     */
    inline fun <reified T : Any> get(
        urlPath: String,
        params: Map<String, Any>?
    ): Mono<T> {
        return get<T>(urlPath, params, null)
    }

    /**
     * Performs a GET HTTP request to the specified URL path.
     * T: Response, U: Request
     *
     * @param urlPath The path to send the GET request to (after /jsf/rfws)
     * @param params Optional query parameters to include in the request
     * @param customHeaders Optional custom headers to include in the request. The default header includes CONTENT_TYPE, AUTHORIZATION, client_id
     * @return A Mono that emits the response body converted to type T
     */
    inline fun <reified T : Any, reified U : Any> get(
        urlPath: String,
        params: U?,
        customHeaders: Map<String, String>?
    ): Mono<T> {

        // convert to multiValueMap<String, String>
        val queryParams = params?.let {
            LinkedMultiValueMap<String, String>().apply {
                U::class.memberProperties.forEach { property ->
                    val key = property.name
                    val value = property.get(params)
                    when (value) {
                        is Collection<*> -> value.forEach { item -> add(key, item.toString()) }
                        else -> add(key, value.toString())
                    }
                }
            }
        } ?: LinkedMultiValueMap<String, String>()

        return get<T>(urlPath, queryParams, customHeaders)
    }

    /**
     * Performs a GET HTTP request to the specified URL path.
     * T: Response, U: Request
     *
     * @param urlPath The path to send the GET request to (after /jsf/rfws)
     * @param params Optional query parameters to include in the request
     * @return A Mono that emits the response body converted to type T
     */
    inline fun <reified T : Any, reified U : Any> get(
        urlPath: String,
        params: U?,
    ): Mono<T> {

        return get<T, U>(urlPath, params, null)
    }

}