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 com.jihe.punchnet.data.ButtonRepository
import com.jihe.punchnet.data.ButtonState
import com.jihe.punchnet.data.RouteItem
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.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.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.FileInputStream
import java.io.FileOutputStream

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)

    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) {
                    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) {
                println("xxx read exception: ${e.message}")
                e.printStackTrace()
                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)

        // Automatically add route for the SDLAN network itself
        val maskInt = com.jihe.punchnet.sdlan.network.netmaskBit2Int(config.ip.netBitLen)
        val networkInt = config.ip.netAddr and maskInt
        val networkStr = ipInt2ByteArray(networkInt).toIPV4String()
        tempVpnDescriptor = tempVpnDescriptor.addRoute(networkStr, config.ip.netBitLen.toInt())

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

        // Apply App Authorization (Split Tunneling)
        val allowedApps = com.jihe.punchnet.data.AppAuthRepository.allowedApps.value
        if (allowedApps.isNotEmpty()) {
            for (pkg in allowedApps) {
                // Ensure we do not add ourselves to the allowed list, otherwise it causes a routing loop
                if (pkg == this.packageName) continue
                try {
                    tempVpnDescriptor = tempVpnDescriptor.addAllowedApplication(pkg)
                } catch (e: android.content.pm.PackageManager.NameNotFoundException) {
                    TerminalLogger.warning { "App not found when setting VPN allowed app: $pkg" }
                }
            }
        } else {
            // If no apps are specified, VPN intercepts everything matching the routes.
            // We ONLY disallow ourselves if there's a 0.0.0.0/0 route, to avoid routing loops.
            // If there's no global route, we want the app (e.g. in-app WebViews) to be able to access the VPN routes!
            val hasGlobalRoute = routes.any { it.mask_ip == 0 }
            if (hasGlobalRoute) {
                try {
                    tempVpnDescriptor = tempVpnDescriptor.addDisallowedApplication(this.packageName)
                    TerminalLogger.infof { "Disallowed app itself to prevent loop on 0.0.0.0/0" }
                } catch (e: Exception) {
                    TerminalLogger.warning { "Failed to disallow self: $e" }
                }
            } else {
                TerminalLogger.infof { "Did not disallow self. In-app WebViews will be routed via VPN." }
            }
        }

        vpnDescriptor = tempVpnDescriptor
            .establish()?.apply {
                input = FileInputStream(fileDescriptor)
                output = FileOutputStream(fileDescriptor)
            }
        ButtonRepository.updateState(ButtonState.ButtonStarted)
    }

    private fun disconnect() {
        synchronized(serviceLock) {
            if (ButtonRepository.buttonState.value != ButtonState.ButtonStarted) {
                return
            }
            /*
            if (!isRunning.get()) {
                return
            }
             */
            ButtonRepository.updateState(ButtonState.ButtonStopping)

            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)
        }
        ButtonRepository.updateState(ButtonState.ButtonStopped)
    }

    private fun connect(startArg: PunchnetServiceArgument?) {
        synchronized(serviceLock) {
            if (ButtonRepository.buttonState.value != ButtonState.ButtonStopped) {
                return
            }
            /*
            if (isRunning.get()) {
                return
            }
             */

            ButtonRepository.updateState(ButtonState.ButtonStarting)

            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:443",
                stun = "$server:1365",
                nat_server1 = "$server:1365",
                nat_server2 = "$server:1366",
                token = startArg?.token?:"",
                name = "tau",
            )

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

            // Initialize UniqueNodeID baseDir to app's files directory
            com.jihe.punchnet.sdlan.utils.UniqueNodeID.setBaseDir(this.filesDir.absolutePath)

            scope.launch {
                val apiClient = com.jihe.punchnet.api.ApiClient()
                val authDataJson = startArg?.token ?: ""
                
                if (authDataJson.isEmpty()) {
                    TerminalLogger.errorf { "API connect failed: No auth data found" }
                    ButtonRepository.updateState(ButtonState.ButtonStopped)
                    return@launch
                }
                
                val authData = try {
                    com.google.gson.Gson().fromJson(authDataJson, com.jihe.punchnet.api.AuthResponseData::class.java)
                } catch (e: Exception) {
                    TerminalLogger.errorf { "API connect failed: Invalid auth data" }
                    ButtonRepository.updateState(ButtonState.ButtonStopped)
                    return@launch
                }

                // 1. Connect directly using the accessToken obtained from LoginScreen
                val connectReq = com.jihe.punchnet.api.ConnectRequest(
                    clientId = "punchnet_android_client_" + com.jihe.punchnet.sdlan.utils.UniqueNodeID.getUUID(this@PunchnetService),
                    accessToken = authData.accessToken
                )
                val connectRes = apiClient.connect(connectReq)
                if (connectRes == null || connectRes.code != 0 || connectRes.data == null) {
                    TerminalLogger.errorf { "API connect failed: ${connectRes?.message}" }
                    ButtonRepository.updateState(ButtonState.ButtonStopped)
                    return@launch
                }
                
                // Update UI state to Started since connection was successful
                ButtonRepository.updateState(ButtonState.ButtonStarted)
                com.jihe.punchnet.data.NodeRepository.updateAll(connectRes.data)
                run_sdlan(scope, iface, argument, authData, connectRes.data)
            }

            // isRunning.set(true)
        }
        // ButtonRepository.updateState(ButtonState.ButtonStarted)
        // buttonViewModel.changeButtonState(ButtonState.ButtonStarted)
    }

    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")
            }
            TerminalLogger.debugf { "service argument received: routes=${argument?.routes?.size ?: 0}, hasToken=${!argument?.token.isNullOrEmpty()}" }
            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()
        com.jihe.punchnet.data.NodeRepository.clear()
        Log.e(TAG, "onDestroy is called")
        // stopForeground(STOP_FOREGROUND_REMOVE)
        super.onDestroy()
    }
}
