package com.sshproxy.vpn.ssh

import com.jcraft.jsch.ChannelDirectTCPIP
import com.jcraft.jsch.Session
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.net.InetSocketAddress
import java.net.ServerSocket
import java.net.Socket
import java.util.concurrent.atomic.AtomicBoolean

/**
 * Локальный SOCKS5: TCP через SSH direct-tcpip, UDP-in-TCP — ретрансляция на
 * hev-socks5-server за SSH (remote host:port с сервера).
 */
class LocalSocks5Proxy(
    private val session: Session,
    private val bindPort: Int,
    private val remoteUdpRelayHost: String,
    private val remoteUdpRelayPort: Int,
    private val remoteSocksCredentials: SocksAuthCredentials = SocksAuthCredentials.NONE
) {
    private var serverSocket: ServerSocket? = null
    private var acceptThread: Thread? = null
    private val running = AtomicBoolean(false)

    fun start(): Int {
        val socket = ServerSocket()
        socket.reuseAddress = true
        socket.bind(InetSocketAddress("127.0.0.1", bindPort))
        serverSocket = socket
        running.set(true)
        acceptThread = Thread {
            while (running.get()) {
                try {
                    val client = socket.accept()
                    Thread({ handleClient(client) }, "socks5-client").apply {
                        isDaemon = true
                        start()
                    }
                } catch (_: IOException) {
                    if (running.get()) {
                        break
                    }
                }
            }
        }.apply {
            isDaemon = true
            name = "socks5-accept"
            start()
        }
        return socket.localPort
    }

    fun stop() {
        running.set(false)
        runCatching { serverSocket?.close() }
        serverSocket = null
        runCatching { acceptThread?.join(2_000) }
        acceptThread = null
    }

    private fun handleClient(client: Socket) {
        client.soTimeout = 60_000
        try {
            client.use { clientSocket ->
                val input = clientSocket.getInputStream()
                val output = clientSocket.getOutputStream()

                if (!readClientGreeting(input, output)) {
                    return
                }

                when (val request = readRequest(input)) {
                    is TcpConnectRequest -> handleTcpConnect(clientSocket, input, output, request)
                    is UdpOverTcpRequest -> handleUdpOverTcp(clientSocket, input, output)
                    null -> writeConnectReply(output, REPLY_GENERAL_FAILURE)
                }
            }
        } catch (_: Exception) {
            // Клиент закрыл соединение или ошибка протокола.
        }
    }

    private fun handleTcpConnect(
        client: Socket,
        @Suppress("UNUSED_PARAMETER") input: InputStream,
        output: OutputStream,
        request: TcpConnectRequest
    ) {
        val channel = session.openChannel("direct-tcpip") as? ChannelDirectTCPIP ?: run {
            writeConnectReply(output, REPLY_GENERAL_FAILURE)
            return
        }
        channel.setHost(request.destination.host)
        channel.setPort(request.destination.port)
        try {
            channel.connect(30_000)
        } catch (_: Exception) {
            writeConnectReply(output, REPLY_HOST_UNREACHABLE)
            return
        }

        writeConnectReply(output, REPLY_SUCCESS)
        relay(client, channel.inputStream, channel.outputStream)
        channel.disconnect()
    }

    private fun handleUdpOverTcp(
        client: Socket,
        @Suppress("UNUSED_PARAMETER") input: InputStream,
        output: OutputStream
    ) {
        val channel = session.openChannel("direct-tcpip") as? ChannelDirectTCPIP ?: run {
            writeConnectReply(output, REPLY_GENERAL_FAILURE)
            return
        }
        channel.setHost(remoteUdpRelayHost)
        channel.setPort(remoteUdpRelayPort)
        try {
            channel.connect(30_000)
        } catch (_: Exception) {
            writeConnectReply(output, REPLY_HOST_UNREACHABLE)
            return
        }

        val remoteIn = channel.inputStream
        val remoteOut = channel.outputStream
        val upstreamAuth = Socks5Protocol.performAuthHandshake(
            remoteIn,
            remoteOut,
            remoteSocksCredentials
        )
        if (!upstreamAuth.success) {
            writeConnectReply(output, REPLY_GENERAL_FAILURE)
            channel.disconnect()
            return
        }
        Socks5Protocol.writeFwdUdpRequest(remoteOut)
        if (!Socks5Protocol.readConnectResponse(remoteIn)) {
            writeConnectReply(output, REPLY_GENERAL_FAILURE)
            channel.disconnect()
            return
        }

        writeConnectReply(output, REPLY_SUCCESS)
        relay(client, remoteIn, remoteOut)
        channel.disconnect()
    }

    private fun readClientGreeting(input: InputStream, output: OutputStream): Boolean {
        val header = ByteArray(2)
        if (!readFully(input, header)) {
            return false
        }
        if (header[0] != SOCKS5_VERSION.toByte()) {
            return false
        }
        val methodCount = header[1].toInt() and 0xFF
        if (methodCount <= 0 || !readFully(input, ByteArray(methodCount))) {
            return false
        }
        output.write(byteArrayOf(SOCKS5_VERSION.toByte(), METHOD_NO_AUTH.toByte()))
        output.flush()
        return true
    }

    private sealed class SocksRequest
    private data class TcpConnectRequest(val destination: Socks5Address) : SocksRequest()
    private object UdpOverTcpRequest : SocksRequest()

    private fun readRequest(input: InputStream): SocksRequest? {
        val header = ByteArray(4)
        if (!readFully(input, header)) {
            return null
        }
        if (header[0] != SOCKS5_VERSION.toByte()) {
            return null
        }

        val cmd = header[1].toInt() and 0xFF
        val destination = readAddress(input, header[3].toInt() and 0xFF) ?: return null

        return when (cmd) {
            CMD_CONNECT -> TcpConnectRequest(destination)
            CMD_FWD_UDP -> UdpOverTcpRequest
            else -> null
        }
    }

    private fun readAddress(input: InputStream, addrType: Int): Socks5Address? {
        return when (addrType) {
            ATYP_IPV4 -> {
                val addr = ByteArray(4)
                val portBytes = ByteArray(2)
                if (!readFully(input, addr) || !readFully(input, portBytes)) {
                    return null
                }
                Socks5Address(
                    host = addr.joinToString(".") { (it.toInt() and 0xFF).toString() },
                    port = readPort(portBytes)
                )
            }

            ATYP_IPV6 -> {
                val addr = ByteArray(16)
                val portBytes = ByteArray(2)
                if (!readFully(input, addr) || !readFully(input, portBytes)) {
                    return null
                }
                Socks5Address(
                    host = addr.joinToString(":") { "%02x".format(it.toInt() and 0xFF) },
                    port = readPort(portBytes)
                )
            }

            ATYP_DOMAIN -> {
                val lengthByte = ByteArray(1)
                if (!readFully(input, lengthByte)) {
                    return null
                }
                val length = lengthByte[0].toInt() and 0xFF
                val domain = ByteArray(length)
                if (!readFully(input, domain)) {
                    return null
                }
                val portBytes = ByteArray(2)
                if (!readFully(input, portBytes)) {
                    return null
                }
                Socks5Address(
                    host = String(domain, Charsets.US_ASCII),
                    port = readPort(portBytes)
                )
            }

            else -> null
        }
    }

    private data class Socks5Address(val host: String, val port: Int)

    private fun writeConnectReply(output: OutputStream, reply: Int) {
        output.write(
            byteArrayOf(
                SOCKS5_VERSION.toByte(),
                reply.toByte(),
                0x00,
                ATYP_IPV4.toByte(),
                0x00,
                0x00,
                0x00,
                0x00,
                0x00,
                0x00
            )
        )
        output.flush()
    }

    private fun relay(client: Socket, remoteIn: InputStream, remoteOut: OutputStream) {
        val clientIn = client.getInputStream()
        val clientOut = client.getOutputStream()

        val toRemote = relayThread("socks5-upstream") {
            copyStream(clientIn, remoteOut)
            runCatching { remoteOut.close() }
            runCatching { client.shutdownOutput() }
        }

        val toClient = relayThread("socks5-downstream") {
            copyStream(remoteIn, clientOut)
            runCatching { clientOut.close() }
            runCatching { client.shutdownInput() }
            runCatching { remoteOut.close() }
        }

        toRemote.start()
        toClient.start()
        runCatching { toRemote.join() }
        runCatching { toClient.join() }
    }

    private fun relayThread(name: String, block: () -> Unit): Thread {
        return Thread({
            try {
                block()
            } catch (_: IOException) {
                // Соединение закрыто.
            }
        }, name).apply { isDaemon = true }
    }

    private fun copyStream(input: InputStream, output: OutputStream) {
        val buffer = ByteArray(16 * 1024)
        while (true) {
            val read = try {
                input.read(buffer)
            } catch (_: IOException) {
                break
            }
            if (read <= 0) {
                break
            }
            try {
                output.write(buffer, 0, read)
                output.flush()
            } catch (_: IOException) {
                break
            }
        }
    }

    private fun readFully(input: InputStream, buffer: ByteArray): Boolean {
        var offset = 0
        while (offset < buffer.size) {
            val read = input.read(buffer, offset, buffer.size - offset)
            if (read < 0) {
                return false
            }
            offset += read
        }
        return true
    }

    private fun readPort(bytes: ByteArray): Int {
        return ((bytes[0].toInt() and 0xFF) shl 8) or (bytes[1].toInt() and 0xFF)
    }

    companion object {
        private const val SOCKS5_VERSION = 0x05
        private const val METHOD_NO_AUTH = 0x00
        private const val CMD_CONNECT = 0x01
        private const val CMD_FWD_UDP = 0x05
        private const val ATYP_IPV4 = 0x01
        private const val ATYP_DOMAIN = 0x03
        private const val ATYP_IPV6 = 0x04

        private const val REPLY_SUCCESS = 0x00
        private const val REPLY_GENERAL_FAILURE = 0x01
        private const val REPLY_HOST_UNREACHABLE = 0x04
    }
}
