package ch.threema.domain.protocol.connection.csp.socket

import ch.threema.base.ThreemaException
import ch.threema.domain.protocol.ServerAddressProvider
import ch.threema.domain.protocol.connection.csp.CspConnectionConfiguration
import ch.threema.domain.stores.IdentityStore
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetSocketAddress
import java.net.UnknownHostException
import java.util.concurrent.ExecutionException
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock

class ChatServerAddressProviderImpl(
    configuration: CspConnectionConfiguration,
) : ChatServerAddressProvider {
    private val identityStore: IdentityStore = configuration.identityStore
    private val serverAddressProvider: ServerAddressProvider = configuration.serverAddressProvider
    private val hostResolver: HostResolver = configuration.hostResolver

    private val lock = ReentrantLock()

    private var socketAddressIndex: Int = 0
    private var serverSocketAddresses: List<InetSocketAddress> = emptyList()

    /**
     * Move the internal pointer to the next available address.
     * If the last address is reached, the pointer will wrap around and start with the first address again.
     */
    override fun advance(): Unit = lock.withLock {
        socketAddressIndex++
        if (socketAddressIndex >= serverSocketAddresses.size) {
            socketAddressIndex = 0
        }
    }

    /**
     * Get the [InetSocketAddress] the internal pointer is currently pointing to.
     */
    override fun get(): InetSocketAddress? = lock.withLock {
        if (socketAddressIndex >= serverSocketAddresses.size) {
            null
        } else {
            serverSocketAddresses[socketAddressIndex]
        }
    }

    /**
     * Update the available [InetSocketAddress]es
     */
    @Throws(
        UnknownHostException::class,
        ExecutionException::class,
        InterruptedException::class,
        ThreemaException::class,
    )
    override fun update(): Unit = lock.withLock {
        val serverHost = getServerHost()

        val ports = serverAddressProvider.getChatServerPorts()
        val addresses = if (ProxyAwareSocketFactory.shouldUseProxy(serverHost, ports[0])) {
            getAddressesWithProxy(serverHost)
        } else {
            getAddressesWithoutProxy(serverHost)
        }

        if (addresses.size != serverSocketAddresses.size || hasChangedAddresses(addresses)) {
            serverSocketAddresses = addresses
            socketAddressIndex = 0
        }
    }

    private fun hasChangedAddresses(addresses: List<InetSocketAddress>): Boolean =
        addresses.withIndex().any {
            val newAddress = it.value.address
            val previousAddress = serverSocketAddresses[it.index].address
            (newAddress == null && previousAddress != null) ||
                (newAddress != null && previousAddress == null) ||
                (newAddress != null && !newAddress.hostAddress.equals(previousAddress.hostAddress))
        }

    private fun getServerHost(): String {
        val serverNamePrefix = serverAddressProvider.getChatServerNamePrefix()
        val serverHost = if (serverNamePrefix.isNotEmpty()) {
            val serverGroup = if (serverAddressProvider.getChatServerUseServerGroups()) identityStore.getServerGroup() else "."
            "$serverNamePrefix$serverGroup"
        } else {
            ""
        }
        return "$serverHost${serverAddressProvider.getChatServerNameSuffix()}"
    }

    private fun getAddressesWithProxy(serverHost: String): List<InetSocketAddress> {
        return serverAddressProvider.getChatServerPorts()
            .map { InetSocketAddress.createUnresolved(serverHost, it) }
    }

    private fun getAddressesWithoutProxy(serverHost: String): List<InetSocketAddress> {
        val inetAddresses = hostResolver.getAllByName(serverHost)
        if (inetAddresses.isEmpty()) {
            throw UnknownHostException()
        }
        inetAddresses.sortWith { o1, o2 ->
            when {
                o1 is Inet6Address && o2 is Inet6Address -> o1.hostAddress.compareTo(o2.hostAddress)
                o1 is Inet6Address -> -1
                o2 is Inet4Address -> o1.hostAddress.compareTo(o2.hostAddress)
                else -> 1
            }
        }
        return inetAddresses.flatMap { address ->
            serverAddressProvider.getChatServerPorts().map { port -> InetSocketAddress(address, port) }
        }
    }
}
