package com.acitelight.aether.service

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.selects.select
import java.io.InputStream
import java.io.OutputStream
import java.net.InetAddress
import java.net.ServerSocket
import java.net.Socket
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.CoroutineContext

@Singleton
class AbyssTunnelProxy @Inject constructor(
    private val settingsDataStoreManager: SettingsDataStoreManager,
    private val authManager: AuthManager
) {
    private val coroutineContext: CoroutineContext = Dispatchers.IO
    private var serverHost: String = ""
    private var serverPort: Int = 0

    fun config(host: String, port: Int)
    {
        serverHost = host
        serverPort = port
    }

    private val listenAddress = InetAddress.getLoopbackAddress()
    private val listenPort = 4095
    private var serverSocket: ServerSocket? = null
    private val scope = CoroutineScope(SupervisorJob() + coroutineContext)

    fun start() {
        serverSocket = ServerSocket(listenPort, 50, listenAddress)
        // accept loop
        scope.launch {
            val srv = serverSocket ?: return@launch
            try {
                while (true) {
                    val client = srv.accept()

                    if(serverHost.isEmpty())
                        continue

                    launch {
                        try { handleLocalConnection(client) }
                        catch (_: Exception) { /* ignore */ }
                    }
                }
            } catch (ex: Exception) {
                println(ex.message)
                // server stopped or fatal error
            } finally {
                stop()
            }
        }
    }

    fun stop() {
        try { serverSocket?.close() } catch (_: Exception) {}
        scope.cancel()
    }

    private suspend fun handleLocalConnection(localSocket: Socket) = withContext(coroutineContext) {
        val localIn = localSocket.getInputStream()
        val localOut = localSocket.getOutputStream()
        var abyssSocket: Socket? = null
        var abyssStream: AbyssStream? = null
        try {
            abyssSocket = Socket(serverHost, serverPort)
            abyssStream = AbyssStream.create(authManager, abyssSocket, authManager.db64(settingsDataStoreManager.privateKeyFlow.first()))

            // concurrently copy in both directions
            val job1 = launch { copyExactSuspend(localIn, abyssStream) }   // local -> abyss
            val job2 = launch { copyFromAbyssToLocal(abyssStream, localOut) } // abyss -> local

            // wait for either direction to finish
            select {
                job1.onJoin { /* completed */ }
                job2.onJoin { /* completed */ }
            }
            // cancel other
            job1.cancel()
            job2.cancel()
        } catch (ex: Exception)
        {
            println(ex.message)
            // log or ignore; we close sockets below
        } finally {
            try { localSocket.close() } catch (_: Exception) {}
            try { abyssStream?.close() } catch (_: Exception) {}
            try { abyssSocket?.close() } catch (_: Exception) {}
        }
        return@withContext
    }

    // Copy from local InputStream into AbyssStream.write in frames.
    private suspend fun copyExactSuspend(localIn: InputStream, abyss: AbyssStream) = withContext(coroutineContext) {
        val buffer = ByteArray(64 * 1024)
        while (true) {
            val read = localIn.read(buffer)
            if (read <= 0)
                break

            abyss.write(buffer, 0, read)
        }
    }

    // Copy from AbyssStream (read frames/decrypt) to local OutputStream
    private suspend fun copyFromAbyssToLocal(abyss: AbyssStream, localOut: OutputStream) = withContext(coroutineContext) {
        val buffer = ByteArray(64 * 1024)
        while (true) {
            val n = abyss.read(buffer, 0, buffer.size)
            if (n <= 0)
                break
            localOut.write(buffer, 0, n)
        }
    }
}