# RetrofitApiCallHandling.kt

    /*
     * The MIT License
     *
     * Original work sponsored and donated by The Danish Health Data Authority (http://www.sundhedsdatastyrelsen.dk)
     *
     * Copyright (C) 2022 The Danish Health Data Authority (http://www.sundhedsdatastyrelsen.dk)
     *
     *
     * Permission is hereby granted, free of charge, to any person obtaining a copy of
     * this software and associated documentation files (the "Software"), to deal in
     * the Software without restriction, including without limitation the rights to
     * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
     * of the Software, and to permit persons to whom the Software is furnished to do
     * so, subject to the following conditions:
     *
     *
     * The above copyright notice and this permission notice shall be included in all
     * copies or substantial portions of the Software.
     *
     *
     * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
     * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
     * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
     * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     * SOFTWARE.
     */

    package dk.nsp.api.util.retrofit

    import com.squareup.moshi.Moshi
    import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
    import dk.nsp.api.util.converters.BigDecimalAdapter
    import dk.nsp.api.util.converters.LocalDateTimeConverter
    import dk.nsp.api.util.converters.UriAdapter
    import dk.nsp.models.ProblemDetails
    import dk.nsp.shared.ApiCallCallFailed
    import dk.nsp.shared.ApiCallCallSuccess
    import dk.nsp.shared.ApiCallResult
    import dk.nsp.shared.ProblemDetailsError
    import okhttp3.Request
    import okio.Timeout
    import retrofit2.*
    import java.lang.reflect.ParameterizedType
    import java.lang.reflect.Type

    /*
       Retrofit adapter factory that creates adapters that will adapt api calls into ApiResult.
       ApiResult can be either ApiCallSuccess or ApiCallFailed. When failed attempt will be made for
       parsing error in response into a ProblemDetails object.
     */
    object ApiCallAdapterFactory : CallAdapter.Factory() {
        override fun get(
            returnType: Type,
            annotations: Array<Annotation>,
            retrofit: Retrofit
        ): CallAdapter<*, *>? {
            // suspend functions wrap the response type in `Call`
            check(Call::class.java == getRawType(returnType)) {
                "Expected ${Call::class.java.canonicalName}, but found ${getRawType(returnType)::class.java.canonicalName}. " +
                        "Return must be wrapped by suspend!"
            }

            // check first that the return type is `ParameterizedType`
            check(returnType is ParameterizedType) {
                "return type must be parameterized as Call<NetworkResponse<<Foo>> or Call<NetworkResponse<out Foo>>"
            }

            // get the response type inside the `Call` type
            val responseType = getParameterUpperBound(0, returnType)
            // if the response type is not ApiResult then we can't handle
            check(getRawType(responseType) == ApiCallResult::class.java) {
                "return type must be of type ${ApiCallResult::class.java.canonicalName}, but was ${
                    getRawType(
                        responseType
                    )
                }"
            }
            // the response type is ApiResponse and should be parameterized
            check(responseType is ParameterizedType) {
                "Response must be parameterized as NetworkResponse<Foo> or NetworkResponse<out Foo>"
            }

            val successBodyType = getParameterUpperBound(0, responseType)
            return ApiCallAdapter<Any>(successBodyType)
        }

        class ApiCallAdapter<S : Any>(
            private val successType: Type
        ) : CallAdapter<S, Call<ApiCallResult<S>>> {

            override fun responseType(): Type = successType

            override fun adapt(call: Call<S>): Call<ApiCallResult<S>> {
                return ApiCall(call)
            }
        }

        private val moshi: Moshi = Moshi
            .Builder()
            .add(BigDecimalAdapter())
            .add(LocalDateTimeConverter())
            .add(UriAdapter())
            .addLast(KotlinJsonAdapterFactory())
            .build()

        internal class ApiCall<S : Any>(private val delegate: Call<S>) : Call<ApiCallResult<out S>> {

            override fun clone(): Call<ApiCallResult<out S>> = ApiCall(delegate)

            override fun execute(): Response<ApiCallResult<out S>> {
                throw UnsupportedOperationException("ResponseCall doesn't support execute")
            }

            override fun isExecuted() = delegate.isExecuted

            override fun isCanceled() = delegate.isCanceled

            override fun cancel() = delegate.cancel()

            override fun request(): Request = delegate.request()

            override fun timeout(): Timeout = delegate.timeout()
            override fun enqueue(callback: Callback<ApiCallResult<out S>>) {
                return delegate.enqueue(object : Callback<S> {
                    override fun onResponse(call: Call<S>, response: Response<S>) {
                        val body = response.body()
                        val code = response.code()
                        val error = response.errorBody()

                        if (response.isSuccessful) {
                            callback.onResponse(
                                this@ApiCall,
                                Response.success(ApiCallCallSuccess(body))
                            )
                        } else {
                            when {
                                error == null -> null
                                error.contentLength() == 0L -> null
                                else -> {
                                    val details = moshi.adapter(ProblemDetails::class.java)
                                        .fromJson(error.string()) as ProblemDetails
                                    callback.onResponse(
                                        this@ApiCall,
                                        Response.success(ApiCallCallFailed(ProblemDetailsError(details)))
                                    )
                                }
                            }

                            callback.onResponse(
                                this@ApiCall,
                                Response.success(ApiCallCallFailed(HttpException(response)))
                            )
                        }
                    }

                    override fun onFailure(call: Call<S>, throwable: Throwable) {
                        callback.onResponse(this@ApiCall, Response.success(ApiCallCallFailed(throwable)))
                    }
                })
            }
        }
    }