package com.jihe.punchnet

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.net.VpnService
import android.os.Build
import android.os.Environment
import android.os.ParcelFileDescriptor
import android.util.Log
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.lifecycle.ViewModelProvider
import com.jihe.punchnet.data.RouteItem
import com.jihe.punchnet.data.RouteViewModel
import com.jihe.punchnet.sdlan.config.Arguments
import com.jihe.punchnet.sdlan.config.toIPV4String
import com.jihe.punchnet.sdlan.logs.TerminalLogger
import com.jihe.punchnet.sdlan.network.ARPTable
import com.jihe.punchnet.sdlan.network.ARPWaitList
import com.jihe.punchnet.sdlan.network.DeviceConfig
import com.jihe.punchnet.sdlan.network.IfaceTun
import com.jihe.punchnet.sdlan.network.RouteDetail
import com.jihe.punchnet.sdlan.network.RouteTable
import com.jihe.punchnet.sdlan.network.StartStopChanInfo
import com.jihe.punchnet.sdlan.network.cidrToRouteDetail
import com.jihe.punchnet.sdlan.network.ipInt2ByteArray
import com.jihe.punchnet.sdlan.network.maskIPToDigit
import com.jihe.punchnet.sdlan.network.run_sdlan
import com.jihe.punchnet.sdlan.utils.ipToString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.concurrent.atomic.AtomicBoolean

class PunchnetService : VpnService() , IfaceTun {
    private val TAG = "PunchnetService"
    private var scope = CoroutineScope(Dispatchers.IO)

    private val routes: MutableList<RouteItem> = mutableListOf()

    private var vpnDescriptor: ParcelFileDescriptor? = null

    private val serviceLock = Any()
    private val isRunning = AtomicBoolean(false)

    private val start_stop_channel = Channel<StartStopChanInfo>(100)

    var input: FileInputStream? = null
    var output: FileOutputStream? = null

    override var arpTable = ARPTable(scope)
    override val arpWaitList = ARPWaitList()

    var config: DeviceConfig = DeviceConfig(1400)

    companion object {
        const val ACTION_CONNECT = "com.jihe.punchnet.punchnetservice.CONNECT"
        const val ACTION_DISCONNECT = "com.jihe.punchnet.punchnetservice.DISCONNECT"
    }

    override fun close() {
        vpnDescriptor?.close()
    }

    override suspend fun recv(): ByteArray {
        val result = withContext(Dispatchers.IO) {
            val result = ByteArray(1500)
            try {
                var size = input?.read(result)
                if (size == null) {
                    println("xxx failed to read")
                    size = 0
                } else {
                    println("xxx got $size bytes")
                }

                // val size = input?.read(result)?:0
                // Log.d(TAG, "RECEIVED $size bytes")
                result.copyOf(size)
                // result.slice(0..<size).toByteArray()
            } catch (e: Exception) {
                ByteArray(0)
            }
        }
        return result
    }

    override suspend fun send(content: ByteArray) {
        withContext(Dispatchers.IO) {
            // Log.d(TAG, "WROTE bytes to vpn service")
            try {
                output?.write(content)
            } catch (e: Exception) {
                TerminalLogger.errorf { "failed to write $e" }
            }
        }
    }

    private fun stopVpn() {
    }

    override suspend fun reload_config(config: DeviceConfig) {
        config.mtu = 1400
        this.config = config
        val ip = ipInt2ByteArray(config.ip.netAddr).toIPV4String()
        TerminalLogger.debugf {"got ip address from remote: ${ip}"}
        withContext(Dispatchers.IO) {
            input?.close()
            output?.close()
        }
        vpnDescriptor?.close()

        var tempVpnDescriptor = Builder()
            .setMtu(config.mtu)
            .addAddress(ip, config.ip.netBitLen.toInt())
            .setBlocking(true)

        for (route in routes) {
            tempVpnDescriptor = tempVpnDescriptor.addRoute(ipToString(route.net_ip), maskIPToDigit(route.mask_ip))
        }

        vpnDescriptor = tempVpnDescriptor
            .establish()

        input = FileInputStream(vpnDescriptor!!.fileDescriptor)
        output = FileOutputStream(vpnDescriptor!!.fileDescriptor)
    }

    private fun disconnect() {
        synchronized(serviceLock) {
            if (!isRunning.get()) {
                return
            }

            Toast.makeText(this, "stop vpn called", Toast.LENGTH_LONG).show()
            input?.close()
            output?.close()
            vpnDescriptor?.close()
            vpnDescriptor = null

            stopForeground(STOP_FOREGROUND_REMOVE)
            stopSelf()
            isRunning.set(false)
        }
    }

    private fun connect(startArg: PunchnetServiceArgument?) {
        synchronized(serviceLock) {
            if (isRunning.get()) {
                return
            }

            val iface = this
            val server = "punchnet.aioe.tech"

            Log.d("DIR", "datadir = ${Environment.getDataDirectory().name}")
            Log.d("DIR", "external storage = ${Environment.getExternalStorageDirectory().name}")
            Log.d("DIR", "filesdir = ${this.filesDir.path}")

            arpTable.routeTable.clearRoute()
            routes.clear()
            for (route in startArg?.routes?.toList()?:listOf()) {
                val r = RouteDetail(
                    mask = route.mask_ip,
                    gw = route.gateway,
                    maskedAddr = route.net_ip,
                )
                routes.add(route)
                // val r = cidrToRouteDetail(route.targetNetCIDR, route.gateway)
                arpTable.routeTable.addRoute(r)
            }

            val argument = Arguments(
                baseDir = this.filesDir.path,
                sn = "$server:1265",
                tcp = "$server:18083",

                nat_server1 = "$server:1265",
                nat_server2 = "47.98.178.3:1265",
                token = startArg?.token?:"",
                name = "tau",
            )

            val notification = createNotification()
            startForeground(1, notification)

            scope.launch {
                run_sdlan(scope, start_stop_channel, iface, argument, startArg)
            }

            isRunning.set(true)
        }
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        super.onStartCommand(intent, flags, startId)

        return if (intent?.action == ACTION_DISCONNECT) {
            scope.cancel()
            disconnect()
            START_STICKY
        } else {

            scope = CoroutineScope(Dispatchers.IO)
            arpTable = ARPTable(scope)

            val argument = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                intent?.getParcelableExtra("argument", PunchnetServiceArgument::class.java)
            } else {
                intent?.getParcelableExtra("argument")
            }
            println("argument = ${argument}")
            connect(argument)
            START_STICKY
        }
        // return super.onStartCommand(intent, flags, startId)
    }

    private fun createNotification(): Notification {
        val channelId = "punchnet_channel"
        val channel = NotificationChannel (
            channelId,
            "punchnet",
            NotificationManager.IMPORTANCE_DEFAULT
        )
        val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        manager.createNotificationChannel(channel)

        return NotificationCompat.Builder(this, channelId)
            .setContentTitle("Punchnet Service")
            .setContentText("Punchnet is running")
            .setSmallIcon(R.drawable.ic_vpn)
            .build()
    }

    override fun onDestroy() {
        // scope.cancel()
        // disconnect()
        scope.cancel()
        disconnect()
        Log.e(TAG, "onDestroy is called")
        // stopForeground(STOP_FOREGROUND_REMOVE)
        super.onDestroy()
    }
}