package pragma.gameinstance.gamefabric

import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsText
import io.ktor.http.URLBuilder
import io.ktor.http.isSuccess
import io.ktor.http.path
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import pragma.applicationError
import pragma.config.ConfigBackendModeFactory
import pragma.config.ConfigObject
import pragma.gameinstance.GameFabricGameInstance
import pragma.gameinstance.gamefabric.GameFabricRegionPingCache.Companion.pingProtocolToProto
import pragma.http.BearerTokenAuthProvider
import pragma.http.TimedHttpClient
import pragma.makeStringApplicationError
import pragma.services.Service
import pragma.settings.BackendType
import pragma.utils.PragmaSerializationUtils

class GameFabricApi(
    private val service: Service,
) {
    class ApiConfig internal constructor(
        backendType: BackendType,
    ) : ConfigObject<ApiConfig>(backendType) {
        override val description = ""
        var gameFabricUrl by types.string("The GameFabric API URL for this studio, e.g. https://mystudio.gamefabric.dev/")
        var allocatePath by types.string("The URL path for the allocate REST endpoint, e.g. 'allocator/prod/us/allocate'")
        var pingPath by types.string("The URL path for the ping REST endpoint, e.g. 'ping'")
        var allocatorToken by types.encryptedString("The encrypted Bearer token to authenticate with GameFabric")
        var gameServerZonesByProviderRegion by types.mapOfString("Maps game fabric region names, e.g. 'us-west', to Pragma game server zones")

        init {
            allocatePath = "allocator/prod/us/allocate"
            pingPath = "ping"
            gameServerZonesByProviderRegion = configMapOf(
                "us-west" to "US_WEST",
                "eu-central-1" to "EU_CENTRAL",
            )
        }

        companion object : ConfigBackendModeFactory<ApiConfig> {
            override fun getFor(type: BackendType): ApiConfig = ApiConfig(type)
        }
    }

    /**
     * https://docs.gamefabric.com/api/multiplayer-servers/allocation-allocator#tag/Allocator
     * {
     *   "attributes": {
     *     "property1": "string",
     *     "property2": "string"
     *   },
     *   "payload": {
     *     "property1": null,
     *     "property2": null
     *   },
     *   "region": "string"
     * }
     */
    @JsonIgnoreProperties(ignoreUnknown = true)
    data class AllocateRequest(
        @JsonProperty("attributes")
        val attributes: Map<String, String>,
        @JsonProperty("payload")
        val payload: Map<String, Any>,
        @JsonProperty("region")
        val region: String,
    )

    /**
     * {
     *     "address": "45.140.130.9:12342",
     *     "ip": "45.140.130.9",
     *     "ports": {
     *         "test": 12342,
     *         "allocator": 13409
     *     },
     *     "token": "625a4ae1-5d68-4112-807e-2aa9a3e36ba1"
     * }
     */
    @JsonIgnoreProperties(ignoreUnknown = true)
    data class AllocateResponse(
        @JsonProperty("address")
        val address: String,
        @JsonProperty("ip")
        val ip: String,
        @JsonProperty("ports")
        val ports: Map<String, String>,
        @JsonProperty("token")
        val token: String,
    )

    @JsonIgnoreProperties(ignoreUnknown = true)
    data class PingServerAddress(
        @JsonProperty("address")
        val address: String,
    )

    @JsonIgnoreProperties(ignoreUnknown = true)
    data class PingRegionEntry(
        @JsonProperty("targets")
        val targets: Map<String, List<PingServerAddress>>, // maps a protocol string, e.g. "icmp", to the server addresses
    )

    @JsonIgnoreProperties(ignoreUnknown = true)
    data class PingServerResponse(
        @JsonProperty("regions")
        val regions: Map<String, PingRegionEntry>, // maps a GameFabric region, e.g. "us-west", to its ping information
    ) {
        fun toProto(gameServerZonesByRegion: Map<String, String>): List<GameFabricGameInstance.PingRegion> =
            regions
                .filter { it.value.targets.isNotEmpty() && gameServerZonesByRegion.containsKey(it.key) }
                .flatMap { (region, regionEntry) ->
                    regionEntry.targets
                        .map { (protocol, addresses) ->
                            GameFabricGameInstance.PingRegion
                                .newBuilder()
                                .setGameServerZone(gameServerZonesByRegion[region])
                                .addAllServers(
                                    addresses.map {
                                        GameFabricGameInstance.PingServer
                                            .newBuilder()
                                            .setProtocol(pingProtocolToProto(protocol))
                                            .setAddress(it.address)
                                            .build()
                                    }
                                ).build()
                        }
                }
    }

    private val httpClient = TimedHttpClient("gamefabric.outgoing.http", service)
    private var logger: Logger = LoggerFactory.getLogger(this::class.simpleName)
    private lateinit var authProvider: BearerTokenAuthProvider
    private lateinit var _config: ApiConfig
    private lateinit var allocateUrl: String
    private lateinit var pingUrl: String

    val config: ApiConfig
        get() = _config

    fun init(config: ApiConfig) {
        this._config = config
        this.authProvider = BearerTokenAuthProvider(config.allocatorToken.getDecryptedValue())
        this.allocateUrl = URLBuilder(config.gameFabricUrl).apply { path(config.allocatePath) }.buildString()
        this.pingUrl = URLBuilder(config.gameFabricUrl).apply { path(config.pingPath) }.buildString()
    }

    /**
     * Executes the REST POST call to GameFabric, and calls failureFunction if the response is an error.
     * https://docs.gamefabric.com/api/multiplayer-servers/allocation-allocator#tag/Allocator/operation/allocate-server
     */
    suspend fun allocate(
        region: String,
        attributes: Map<String, String>,
        payload: Map<String, Any>,
        failureFunction: suspend (response: HttpResponse) -> AllocateResponse = { response ->
            applicationError { makeStringApplicationError("GameFabric allocation failed: ${response.status.description}") }
        },
    ): AllocateResponse {
        val requestBody =
            AllocateRequest(
                attributes = attributes,
                payload = payload,
                region = region
            )
        val requestBodyJson = PragmaSerializationUtils.toJsonString(requestBody)

        val response =
            httpClient.post(
                allocateUrl,
                requestBodyJson,
                auth = authProvider,
                headers = mapOf("Content-Type" to "application/json"),
                expectSuccess = false
            )
        if (response.status.isSuccess()) {
            val responseJson = response.bodyAsText()
            logger.debug("DEBUG - GameFabric Allocation Response: {}", responseJson)
            val allocateResponse = PragmaSerializationUtils.fromJsonToObject<AllocateResponse>(responseJson)
            return allocateResponse
        } else {
            logger.error("GameFabric allocation error. Status: ${response.status}, Body: ${response.bodyAsText()}")
            return failureFunction(response)
        }
    }

    /**
     * Executes the REST GET call to fetch GameFabric ping servers configured for an environment
     * https://docs.gamefabric.com/api/multiplayer-servers/allocation-allocator#tag/Allocator/operation/ping???
     */
    suspend fun ping(
        failureFunction: suspend (response: HttpResponse) -> PingServerResponse = { response ->
            applicationError { makeStringApplicationError("GameFabric ping server lookup failed: ${response.status.description}") }
        },
    ): PingServerResponse {
        val response =
            httpClient.get(
                pingUrl,
                auth = authProvider,
                expectSuccess = false
            )
        if (response.status.isSuccess()) {
            val responseJson = response.bodyAsText()
            val pingResponse = PragmaSerializationUtils.fromJsonToObject<PingServerResponse>(responseJson)
            logger.debug("DEBUG - GameFabric ping Response: {}", responseJson)
            return pingResponse
        } else {
            logger.error("GameFabric ping server lookup error. Status: ${response.status}, Body: ${response.bodyAsText()}")
            return failureFunction(response)
        }
    }
}
