package pragma.gameinstance.gamefabric

import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsText
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import pragma.ApplicationErrorException
import pragma.applicationError
import pragma.client.PartnerClientTokens
import pragma.config.ConfigBackendModeFactory
import pragma.config.PluginConfig
import pragma.content.ContentDataNodeService
import pragma.gameinstance.ExtAllocateGameServer
import pragma.gameinstance.GameFabricGameInstance
import pragma.gameinstance.GameServerProviderPlugin
import pragma.gameinstance.gamefabric.GameFabricApi.ApiConfig
import pragma.gameinstance.gamefabric.GameFabricGameServerProviderPlugin.Config
import pragma.plugins.ConfigurablePlugin
import pragma.services.Service
import pragma.settings.BackendType
import pragma.utils.GameInstanceId
import pragma.utils.GameServerVersion
import pragma.utils.GameServerZone
import pragma.utils.toFixed128

class GameFabricGameServerProviderPlugin(
    override val service: Service,
    override val contentDataNodeService: ContentDataNodeService,
    private val gameFabricApi: GameFabricApi,
) : GameServerProviderPlugin,
    ConfigurablePlugin<Config> {
    internal constructor(
        service: Service,
        contentDataNodeService: ContentDataNodeService,
    ) : this(service, contentDataNodeService, GameFabricApi(service))

    class Config private constructor(
        type: BackendType,
    ) : PluginConfig<Config>(type) {
        override val description = "GameFabric GameServerProviderPlugin configuration."

        var gameFabric by types.embeddedObject(ApiConfig::class, "Configuration for getting ping servers from GameFabric")
        var pragmaGamePartnerBackendAddress by types.string("Tells the game server where the Pragma Game Partner gateway is")
        var fallbackRegion by types.string("The fallback region if unable to allocate in the desired region")
        var fallbackEnvironment by types.string("The fallback environment if unable to extract the environment from the game server version")
        var fallbackArmadaSet by types.string("The fallback armada set if unable to extract the armada set from the game server version")

        init {
            fallbackRegion = ""
            fallbackEnvironment = ""
            fallbackArmadaSet = ""
        }

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

    private lateinit var config: Config
    private val logger: Logger = LoggerFactory.getLogger(this::class.simpleName)

    override suspend fun onConfigChanged(config: Config) {
        this.config = config
        gameFabricApi.init(config.gameFabric)
    }

    override suspend fun startAllocationForGameInstance(
        gameInstanceId: GameInstanceId,
        ext: ExtAllocateGameServer,
        clientTokenFunction: () -> PartnerClientTokens,
        gameServerVersion: GameServerVersion,
        gameServerZone: GameServerZone,
    ) {
        val tokens = clientTokenFunction()

        // Parse identifiers passed through the Ext payload
        val environment = if (ext.gameServerVersion.isEmpty()) config.fallbackEnvironment else ext.gameServerVersion.substringBefore("::")
        val armadaSet = ext.gameServerVersion.substringAfter("::", config.fallbackArmadaSet)
        val primaryRegion = ext.desiredRegion.lowercase().ifBlank { config.fallbackRegion.lowercase() }

        logger.debug(
            "startAllocationForGameInstance - region: {}, environment: {}, armada set: {}, serverArgs: {}, (gameServerVersion: {})",
            primaryRegion,
            environment,
            armadaSet,
            ext.serverArgsMap,
            ext.gameServerVersion
        )

        // 1. Attributes for GameFabric to find the correct fleet
        val attributes =
            mutableMapOf(
                "env" to environment,
                "armadaset" to armadaSet
            )

        // 2. Payload for the Unreal game server (translated into Agones annotations)
        val payload =
            mutableMapOf<String, Any>(
                "GameInstanceId" to gameInstanceId.toString(),
                "PartnerGameToken" to tokens.gameToken,
                "PartnerSocialToken" to tokens.socialToken,
                "pragmaBackendAddress" to config.pragmaGamePartnerBackendAddress,
                "ServerArgs" to ext.serverArgsMap
            )

        var region = primaryRegion

        // DRY allocation failure handler
        val onFailure: suspend (String, HttpResponse?) -> Nothing = { message, response ->
            throwAllocationError(
                message,
                gameInstanceId,
                ext,
                gameServerVersion,
                gameServerZone,
                response
            )
        }

        // Attempt allocation to the primary region and, if requested, fallback region
        runCatching {
            gameFabricApi.allocate(primaryRegion, attributes, payload, { response ->
                logger.error("GameFabric allocation failed for $gameInstanceId in $primaryRegion (${response.status})")

                // attempt allocation in fallback region if configured, otherwise fail
                if (config.fallbackRegion.isNotBlank()) {
                    region = config.fallbackRegion.lowercase()
                    logger.debug("Attempting to allocate in the configured fallback region: $region")
                    gameFabricApi.allocate(region, attributes, payload, { fallbackResponse ->
                        logger.error("GameFabric allocation failed for $gameInstanceId in fallback region: $region (${fallbackResponse.status}).")
                        onFailure("Failed to allocate game server in either $primaryRegion or $region.", fallbackResponse)
                    })
                } else {
                    onFailure("Failed to allocate game server in $primaryRegion.", response)
                }
            })
        }.getOrElse { e ->
            if (e is ApplicationErrorException) throw e
            logger.error("Failed to invoke GameFabric allocation for $gameInstanceId", e)
            onFailure("Failed to invoke GameFabric allocation request: $e", null)
        }

        regionMetrics(region)
    }

    private suspend fun throwAllocationError(
        message: String,
        gameInstanceId: GameInstanceId,
        ext: ExtAllocateGameServer,
        gameServerVersion: GameServerVersion,
        gameServerZone: GameServerZone,
        response: HttpResponse? = null,
    ): Nothing =
        applicationError {
            GameFabricGameInstance.GameServerAllocationApplicationError
                .newBuilder()
                .setMessage(message)
                .setAllocationData(
                    GameFabricGameInstance.GameServerAllocationData
                        .newBuilder()
                        .setGameInstanceId(gameInstanceId.toFixed128())
                        .setExt(ext)
                        .setGameServerVersion(gameServerVersion)
                        .setGameServerZone(gameServerZone)
                ).apply {
                    // Only set HttpResponseData if the response is not null
                    response?.let {
                        setHttpResponseData(
                            GameFabricGameInstance.HttpResponseData
                                .newBuilder()
                                .setResponseCode(it.status.value)
                                .setResponseDesc(it.status.description)
                                .setResponseBody(it.bodyAsText())
                        )
                    }
                }.build()
        }

    private fun regionMetrics(region: String) {
        service.pragmaNode
            .metricsCounter(
                "nsg.performance.games",
                Pair("region", region)
            ).increment()
    }
}
