package com.ffii.fpsms.api.service

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.ffii.core.utils.JwtTokenUtil
import com.ffii.fpsms.m18.M18Config
import com.ffii.fpsms.m18.model.M18TokenRequest
import com.ffii.fpsms.m18.model.M18TokenResponse
import jakarta.annotation.PostConstruct
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
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 reactor.core.publisher.Mono
import kotlin.reflect.full.memberProperties
import org.springframework.context.annotation.Lazy
import org.springframework.http.HttpMethod
import org.springframework.http.HttpRequest
import org.springframework.http.HttpStatusCode
import org.springframework.core.io.buffer.DataBuffer
import org.springframework.core.io.buffer.DefaultDataBufferFactory
import org.springframework.web.reactive.function.BodyInserters
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import org.springframework.web.reactive.function.client.ClientRequest
import org.springframework.web.reactive.function.client.ExchangeFilterFunction
import org.springframework.web.reactive.function.client.WebClientResponseException
import org.springframework.web.service.invoker.HttpRequestValues
import reactor.util.retry.Retry
import java.net.URI
import java.time.Duration
import java.util.concurrent.atomic.AtomicReference

@Service
open class ApiCallerService(
    val m18Config: M18Config,
) {
    val logger: Logger = LoggerFactory.getLogger(ApiCallerService::class.java)
    private val MAX_IN_MEMORY_SIZE = 10 * 1024 * 1024
    val DEFAULT_RETRY_ATTEMPTS = 5L

    val webClient: WebClient = WebClient.builder()
        .baseUrl(m18Config.BASE_URL)
//        .baseUrl(m18Config.BASE_URL_UAT)
        .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)
        }
        .filter(ExchangeFilterFunction.ofRequestProcessor { request ->
//            logger.info("Request: ${request.method()} ${request.url()}")
            Mono.just(request)
        }.andThen(ExchangeFilterFunction.ofResponseProcessor { response ->
//            logger.info("Response: ${response.statusCode()}")
            Mono.just(response)
        }))
        .codecs { configurer -> configurer.defaultCodecs().maxInMemorySize(MAX_IN_MEMORY_SIZE) }
        .build()

    @PostConstruct
    fun init() {
        updateToken().subscribe()
    }

    fun updateToken() : Mono<String>{
        val params = M18TokenRequest(
            grant_type = m18Config.GRANT_TYPE,
            client_id = m18Config.CLIENT_ID,
            client_secret = m18Config.CLIENT_SECRET,
            username = m18Config.USERNAME,
            password = m18Config.PASSWORD
        )

        return get<M18TokenResponse, M18TokenRequest>("/oauth/token", params, null)
            .doOnSuccess { response ->
                if (m18Config.ACCESS_TOKEN != response.access_token) {
                    m18Config.ACCESS_TOKEN = response.access_token
                    logger.info("Token updated: ${response.access_token}")
                }
            }
            .doOnError { error -> logger.error("Token update failed: ${error.message}") }
            .map { it.access_token }
    }

    // ------------------------------------ GET ------------------------------------ //
    /**
     * Performs a GET HTTP request to the specified URL path with parameters.
     * T: Response, U: Request
     *
     * @param urlPath The path to send the GET request to (after /jsf/rfws)
     * @param params Optional query parameters (supports Map, MultiValueMap, or custom object)
     * @param customHeaders Optional custom headers 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? = null,
        customHeaders: Map<String, String>? = null
    ): Mono<T> {
        val queryParams = when (params) {
            is Map<*, *> -> LinkedMultiValueMap<String, String>().apply {
                params.forEach { (k, v) ->
                    when (v) {
                        is Collection<*> -> addAll(k.toString(), v.map { it.toString() })
                        else -> add(k.toString(), v.toString())
                    }
                }
            }
            null -> LinkedMultiValueMap<String, String>()
            else -> 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())
                    }
                }
            }
        }

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

    /**
     * Performs a GET HTTP request to the specified URL path without parameters.
     *
     * @param urlPath The path to send the GET request to (after /jsf/rfws)
     * @param customHeaders Optional custom headers 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>?,
        customHeaders: Map<String, String>? = null
    ): 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 executeGet<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
     * @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> executeGet(
        urlPath: String,
        params: MultiValueMap<String, String>,
        customHeaders: Map<String, String>?
    ): Mono<T> {
        println("ACCESS TOKEN: ${m18Config.ACCESS_TOKEN}")

        return webClient.get()
            .uri { uriBuilder -> uriBuilder.path(urlPath).queryParams(params).build() }
            .headers { headers -> customHeaders?.forEach { (k, v) -> headers.set(k, v) } }
            .retrieve()
            .bodyToMono(T::class.java)
            // Below is for test bug (200 but error). Need to comment out bodyToMono and add jsonIgnore to data class
//            .toEntity(String::class.java) // Get raw body as String
//            .flatMap { entity ->
//                logger.info("Response Status: ${entity.statusCode}")
//                logger.info("Response Headers: ${entity.headers}")
//                logger.info("Raw Response Body: ${entity.body}")
//                try {
//                    val objectMapper = jacksonObjectMapper()
//                    val deserialized = objectMapper.readValue(entity.body, T::class.java)
//                    Mono.just(deserialized)
//                } catch (e: Exception) {
//                    logger.error("Deserialization failed: ${e.message}")
//                    Mono.error(e)
//                }
//            }
            .doOnError { error -> println("Error occurred: ${error.message}") }
            .onErrorResume(WebClientResponseException::class.java) { error ->
                logger.error("WebClientResponseException: ${error.statusCode} - ${error.statusText}, Body: ${error.responseBodyAsString}")
                if (error.statusCode == HttpStatusCode.valueOf(400) || error.statusCode == HttpStatusCode.valueOf(401)) {
                    updateToken().flatMap { newToken ->
                        webClient.get()
                            .uri { uriBuilder -> uriBuilder.path(urlPath).queryParams(params).build() }
                            .headers { headers ->
                                headers.set(HttpHeaders.AUTHORIZATION, "Bearer $newToken")
                                customHeaders?.forEach { (k, v) -> headers.set(k, v) }
                            }
                            .retrieve()
                            .bodyToMono(T::class.java)
                    }
                } else {
                    Mono.error(error)
                }
            }
            .retryWhen(
                Retry.backoff(DEFAULT_RETRY_ATTEMPTS, Duration.ofSeconds(1))
//                    .filter { it is WebClientResponseException && it.statusCode != HttpStatusCode.valueOf(400) }
                    .doBeforeRetry { signal -> logger.info("Retrying due to: ${signal.failure().message}") }
            )
    }

    // ------------------------------------ POST ------------------------------------ //
    /**
     * Performs a POST HTTP request to the specified URL path with parameters.
     * T: Response, U: Request
     *
     * @param urlPath The path to send the POST request to (after /jsf/rfws)
     * @param params Optional query parameters (supports Map, MultiValueMap, or custom object)
     * @param customHeaders Optional custom headers 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> post(
        urlPath: String,
        params: U? = null,
        customHeaders: Map<String, String>? = null
    ): Mono<T> {
        val queryParams = when (params) {
            is Map<*, *> -> LinkedMultiValueMap<String, String>().apply {
                params.forEach { (k, v) ->
                    when (v) {
                        is Collection<*> -> addAll(k.toString(), v.map { it.toString() })
                        else -> add(k.toString(), v.toString())
                    }
                }
            }
            null -> LinkedMultiValueMap<String, String>()
            else -> 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())
                    }
                }
            }
        }

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

    /**
     * Performs a POST HTTP request to the specified URL path without parameters.
     *
     * @param urlPath The path to send the POST request to (after /jsf/rfws)
     * @param customHeaders Optional custom headers to include in the request
     * @return A Mono that emits the response body converted to type T
     */
    inline fun <reified T : Any> post(
        urlPath: String,
        params: Map<String, Any>?,
        customHeaders: Map<String, String>? = null
    ): 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 executePost<T>(urlPath, queryParams, customHeaders)
    }

    /**
     * Performs a POST 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> executePost(
        urlPath: String,
        params: MultiValueMap<String, String>,
        customHeaders: Map<String, String>?
    ): Mono<T> {
        println("ACCESS TOKEN: ${m18Config.ACCESS_TOKEN}")

        return webClient.post()
            .uri { uriBuilder -> uriBuilder.path(urlPath).build() }
            .headers { headers -> customHeaders?.forEach { (k, v) -> headers.set(k, v) } }
            .body(BodyInserters.fromValue(params))
            .retrieve()
            .bodyToMono(T::class.java)
            .doOnError { error -> println("Error occurred: ${error.message}") }
            .onErrorResume(WebClientResponseException::class.java) { error ->
                logger.error("WebClientResponseException: ${error.statusCode} - ${error.statusText}, Body: ${error.responseBodyAsString}")
                if (error.statusCode == HttpStatusCode.valueOf(400) || error.statusCode == HttpStatusCode.valueOf(401)) {
                    updateToken().flatMap { newToken ->
                        webClient.get()
                            .uri { uriBuilder -> uriBuilder.path(urlPath).queryParams(params).build() }
                            .headers { headers ->
                                headers.set(HttpHeaders.AUTHORIZATION, "Bearer $newToken")
                                customHeaders?.forEach { (k, v) -> headers.set(k, v) }
                            }
                            .retrieve()
                            .bodyToMono(T::class.java)
                    }
                } else {
                    Mono.error(error)
                }
            }
            .retryWhen(
                Retry.backoff(DEFAULT_RETRY_ATTEMPTS, Duration.ofSeconds(1))
                    .doBeforeRetry { signal -> logger.info("Retrying due to: ${signal.failure().message}") }
            )
    }

    // ------------------------------------ PUT ------------------------------------ //
    /**
     * Performs a PUT HTTP request with a pre-serialized JSON string body.
     * Sends the body as explicit UTF-8 bytes. Use Content-Type application/json only (no charset)
     * so M18 persists bDesc/bDesc_en like when sent from Postman.
     */
    inline fun <reified T : Any> putWithJsonString(
        urlPath: String,
        queryParams: MultiValueMap<String, String>,
        bodyJson: String,
        customHeaders: Map<String, String>? = null
    ): Mono<T> {
        val contentType = MediaType.APPLICATION_JSON
        val utf8Bytes = bodyJson.toByteArray(StandardCharsets.UTF_8)
        val bodyBuffer: DataBuffer = DefaultDataBufferFactory.sharedInstance.wrap(utf8Bytes)
        return webClient.put()
            .uri { uriBuilder -> uriBuilder.path(urlPath).queryParams(queryParams).build() }
            .contentType(contentType)
            .headers { headers -> customHeaders?.forEach { (k, v) -> headers.set(k, v) } }
            .body(BodyInserters.fromDataBuffers(Mono.just(bodyBuffer)))
            .retrieve()
            .bodyToMono(T::class.java)
            .doOnError { error -> logger.error("PUT error: ${error.message}") }
            .onErrorResume(WebClientResponseException::class.java) { error ->
                logger.error("WebClientResponseException: ${error.statusCode} - ${error.statusText}, Body: ${error.responseBodyAsString}")
                if (error.statusCode == HttpStatusCode.valueOf(400) || error.statusCode == HttpStatusCode.valueOf(401)) {
                    updateToken().flatMap { newToken ->
                        val retryBuffer = DefaultDataBufferFactory.sharedInstance.wrap(utf8Bytes)
                        webClient.put()
                            .uri { uriBuilder -> uriBuilder.path(urlPath).queryParams(queryParams).build() }
                            .header(HttpHeaders.AUTHORIZATION, "Bearer $newToken")
                            .contentType(contentType)
                            .headers { headers -> customHeaders?.forEach { (k, v) -> headers.set(k, v) } }
                            .body(BodyInserters.fromDataBuffers(Mono.just(retryBuffer)))
                            .retrieve()
                            .bodyToMono(T::class.java)
                    }
                } else {
                    Mono.error(error)
                }
            }
    }

    /**
     * Performs a PUT with the JSON payload in the "param" query parameter (URL-encoded) and empty body.
     * Builds the URI manually so JSON braces are not treated as URI template variables.
     */
    inline fun <reified T : Any> putWithParamJson(
        urlPath: String,
        menuCode: String,
        bodyJson: String,
        customHeaders: Map<String, String>? = null
    ): Mono<T> {
        val encodedParam = URLEncoder.encode(bodyJson, StandardCharsets.UTF_8).replace("+", "%20")
        val base = m18Config.BASE_URL.removeSuffix("/")
        val path = urlPath.removePrefix("/")
        val fullUri = "$base/$path?menuCode=$menuCode&param=$encodedParam"
        val uri = URI.create(fullUri)
        return webClient.put()
            .uri(uri)
            .headers { headers -> customHeaders?.forEach { (k, v) -> headers.set(k, v) } }
            .bodyValue("")
            .retrieve()
            .bodyToMono(T::class.java)
            .doOnError { error -> logger.error("PUT (param) error: ${error.message}") }
            .onErrorResume(WebClientResponseException::class.java) { error ->
                logger.error("WebClientResponseException: ${error.statusCode} - ${error.statusText}, Body: ${error.responseBodyAsString}")
                if (error.statusCode == HttpStatusCode.valueOf(400) || error.statusCode == HttpStatusCode.valueOf(401)) {
                    updateToken().flatMap { newToken ->
                        webClient.put()
                            .uri(uri)
                            .header(HttpHeaders.AUTHORIZATION, "Bearer $newToken")
                            .headers { headers -> customHeaders?.forEach { (k, v) -> headers.set(k, v) } }
                            .bodyValue("")
                            .retrieve()
                            .bodyToMono(T::class.java)
                    }
                } else {
                    Mono.error(error)
                }
            }
    }

    /**
     * Performs a PUT HTTP request to the specified URL path with query parameters and JSON body.
     *
     * @param urlPath The path to send the PUT request to (e.g. /root/api/save/an)
     * @param queryParams Query parameters (e.g. menuCode=an, param optional)
     * @param body The request body object (serialized as JSON)
     * @param customHeaders Optional custom headers
     * @return A Mono that emits the response body converted to type T
     */
    inline fun <reified T : Any> put(
        urlPath: String,
        queryParams: MultiValueMap<String, String>,
        body: Any,
        customHeaders: Map<String, String>? = null
    ): Mono<T> {
        return webClient.put()
            .uri { uriBuilder -> uriBuilder.path(urlPath).queryParams(queryParams).build() }
            .headers { headers -> customHeaders?.forEach { (k, v) -> headers.set(k, v) } }
            .bodyValue(body)
            .retrieve()
            .bodyToMono(T::class.java)
            .doOnError { error -> logger.error("PUT error: ${error.message}") }
            .onErrorResume(WebClientResponseException::class.java) { error ->
                logger.error("WebClientResponseException: ${error.statusCode} - ${error.statusText}, Body: ${error.responseBodyAsString}")
                if (error.statusCode == HttpStatusCode.valueOf(400) || error.statusCode == HttpStatusCode.valueOf(401)) {
                    updateToken().flatMap { newToken ->
                        webClient.put()
                            .uri { uriBuilder -> uriBuilder.path(urlPath).queryParams(queryParams).build() }
                            .header(HttpHeaders.AUTHORIZATION, "Bearer $newToken")
                            .headers { headers -> customHeaders?.forEach { (k, v) -> headers.set(k, v) } }
                            .bodyValue(body)
                            .retrieve()
                            .bodyToMono(T::class.java)
                    }
                } else {
                    Mono.error(error)
                }
            }
    }
}