package com.example.modifier

import android.Manifest
import android.annotation.SuppressLint
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.telephony.SubscriptionManager
import android.util.Log
import androidx.core.content.ContextCompat
import com.example.modifier.constants.CMD_HOME
import com.example.modifier.constants.CMD_MESSAGING_APP
import com.example.modifier.constants.CMD_START_PLAY_STORE
import com.example.modifier.constants.PACKAGE_GMS
import com.example.modifier.constants.PACKAGE_GSF
import com.example.modifier.constants.PACKAGE_MESSAGING
import com.example.modifier.data.BackupItem
import com.example.modifier.data.BackupItemDao
import com.example.modifier.extension.clear
import com.example.modifier.extension.disable
import com.example.modifier.extension.enable
import com.example.modifier.extension.kill
import com.example.modifier.extension.resume
import com.example.modifier.extension.suspend
import com.example.modifier.model.TelephonyConfig
import com.example.modifier.serializer.Json
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.RandomStringUtils
import java.io.File
import java.nio.file.Files
import java.util.Date
import java.util.Optional


object Global {
    @JvmField
    var telephonyConfig: TelephonyConfig = TelephonyConfig(
        number = "",
        mcc = "",
        mnc = "",
        iccid = "",
        imsi = "",
        imei = "",
        country = "",
        areaCode = "",
        available = false,
        carrierId = "1",
        carrierName = "T-Mobile"
    )

    private const val TAG = "Modifier"

    @JvmStatic
    fun load() {
        val context = Utils.getContext()
        try {
            val file = File(ContextCompat.getDataDir(context), "config.json")
            if (file.exists()) {
                val gson = Gson()
                val json = FileUtils.readFileToString(file, "UTF-8")
                telephonyConfig = gson.fromJson(json, TelephonyConfig::class.java)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    @JvmStatic
    val servers: MutableSet<String>
        get() {
            val context = Utils.getContext()
            val defServers: MutableSet<String> = HashSet()
            defServers.add("http://47.98.225.28")
            defServers.add("http://8.149.128.251")
            defServers.add("http://192.168.6.215:3000")
            defServers.add("http://192.168.50.135:3000")
            val prefs =
                context.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE)
            return defServers
        }

    @JvmStatic
    fun save() {
        val context = Utils.getContext()
        val file = File(ContextCompat.getDataDir(context), "config.json")
        FileUtils.write(file, Json.encodeToString(telephonyConfig), "UTF-8")
    }

    @SuppressLint("MissingPermission")
    @JvmStatic
    suspend fun save(telephonyConfig: TelephonyConfig, suspend: Boolean? = true) {
        Global.telephonyConfig.mcc = telephonyConfig.mcc
        Global.telephonyConfig.mnc = telephonyConfig.mnc
        Global.telephonyConfig.number = telephonyConfig.number
        Global.telephonyConfig.country = telephonyConfig.country
        Global.telephonyConfig.areaCode = telephonyConfig.areaCode
        Global.telephonyConfig.iccid = telephonyConfig.iccid
        Global.telephonyConfig.imei = telephonyConfig.imei
        Global.telephonyConfig.imsi = telephonyConfig.imsi
        Global.telephonyConfig.available = telephonyConfig.available
        Global.telephonyConfig.carrierId = telephonyConfig.carrierId
        Global.telephonyConfig.carrierName = telephonyConfig.carrierName

        try {
            if (suspend == true) {
                suspendPackage(PACKAGE_GMS, PACKAGE_MESSAGING)
            }
            save()

            shellRun(
                "setprop persist.spoof.mcc ${telephonyConfig.mcc}",
                "setprop persist.spoof.mnc ${telephonyConfig.mnc}",
                "setprop persist.spoof.number ${telephonyConfig.number}",
                "setprop persist.spoof.country ${telephonyConfig.country}",
                "setprop persist.spoof.iccid ${telephonyConfig.iccid}",
                "setprop persist.spoof.imei ${telephonyConfig.imei}",
                "setprop persist.spoof.imsi ${telephonyConfig.imsi}",
                "setprop persist.spoof.carrier.id ${
                    telephonyConfig.carrierId.replace(
                        "^\\W*\$".toRegex(),
                        "''"
                    )
                }",
                "setprop persist.spoof.carrier.name ${
                    telephonyConfig.carrierName.replace(
                        "^\\W*\$".toRegex(),
                        "''"
                    )
                }",
            )

            val context = Utils.getContext()
            val subscriptionManager: SubscriptionManager =
                context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE) as SubscriptionManager

            if (hasPermission(Manifest.permission.READ_PHONE_STATE)) {
                val simCount = subscriptionManager.activeSubscriptionInfoCountMax
                for (i in 0 until simCount) {
                    val info = subscriptionManager.getActiveSubscriptionInfoForSimSlotIndex(i)
                    if (info != null) {
                        val mcc = info.mccString
                        val mnc = info.mncString
                        Log.i(TAG, "mccmnc spoofed: $mcc$mnc")
                    }
                }
            }
            if (suspend == true) {
                resumePackage(PACKAGE_GMS, PACKAGE_MESSAGING)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    private fun isOldVersion(): Boolean {
        val info =
            Utils.getContext().packageManager.getPackageInfo("com.google.android.apps.messaging", 0)
        var oldVersion = false
        if (info != null) {
            if (info.versionCode < 170545910) {
                oldVersion = true
            }
        }
        return oldVersion
    }

    suspend fun saveMock() {
        if (isOldVersion()) {
            val content = Utils.getContext().assets.open("us_numbers.txt").bufferedReader().use {
                it.readText()
            }
            // get random number
            content.split("\n")
                .filter { it.isNotBlank() }
                .shuffled()
                .firstOrNull()
                ?.let {
                    save(
                        TelephonyConfig(
                            number = it,
                            mcc = "310",
                            mnc = "150",
                            iccid = genICCID("310", "1"),
                            "310150" + RandomStringUtils.randomNumeric(9),
                            Utils.generateIMEI(),
                            "us",
                            "1",
                            available = false,
                            carrierId = "1779",
                            carrierName = "Cricket Wireless"
                        )
                    )
                }
        } else {
            val content = Utils.getContext().assets.open("us_numbers.txt").bufferedReader().use {
                it.readText()
            }
            // get random number
            content.split("\n")
                .filter { it.isNotBlank() }
                .shuffled()
                .firstOrNull()
                ?.let {
                    save(
                        TelephonyConfig(
                            number = it,
                            mcc = "310",
                            mnc = "240",
                            iccid = genICCID("310", "1"),
                            "310240" + RandomStringUtils.randomNumeric(9),
                            Utils.generateIMEI(),
                            "us",
                            "1",
                            available = false,
                            "1",
                            "T-Mobile"
                        )
                    )
                }
        }
    }

    @JvmStatic
    suspend fun clear(gsf: Boolean, gms: Boolean, sms: Boolean) {
        try {
            suspendPackage(PACKAGE_GSF, PACKAGE_GMS, PACKAGE_MESSAGING)
            val cmds: MutableList<String> = ArrayList()
            // suspend
            if (gsf) {
                cmds.add(PACKAGE_GSF.suspend())
                cmds.add(PACKAGE_GSF.kill())
                cmds.add("echo 'gsf suspended'")
            }
            if (gms) {
                cmds.add(PACKAGE_GMS.suspend())
                cmds.add(PACKAGE_GMS.kill())
                cmds.add("echo 'gms suspended'")
            }
            if (sms) {
                cmds.add(PACKAGE_MESSAGING.suspend())
                cmds.add(PACKAGE_MESSAGING.kill())
                cmds.add("echo 'sms suspended'")
            }
            cmds.add("sleep 1")
            // clear
            if (gsf) {
                cmds.add(PACKAGE_GSF.clear())
                cmds.add("echo 'cleared gsf'")
            }
            if (gms) {
                cmds.add(PACKAGE_GMS.clear())
                cmds.add("echo 'cleared gms'")
            }
            if (sms) {
                cmds.add(PACKAGE_MESSAGING.clear())
                cmds.add("echo 'cleared sms'")
            }
            cmds.add("sleep 1")
            // unsuspend
            if (gsf) {
                cmds.add(PACKAGE_GSF.resume())
                cmds.add("echo 'gsf unsuspend'")
            }
            if (gms) {
                cmds.add(PACKAGE_GMS.resume())
                cmds.add("echo 'gms unsuspend'")
            }
            if (sms) {
                cmds.add(PACKAGE_MESSAGING.resume())
                cmds.add("echo 'sms unsuspend'")
            }
            cmds.add("sleep 1")
            shellRun(*cmds.toTypedArray<String>())
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

//    @JvmStatic
//    fun stop(gsf: Boolean? = false, gms: Boolean? = false, sms: Boolean? = false) {
//        try {
//            val cmds: MutableList<String> = ArrayList()
//            if (gsf == true) {
//                cmds.add(PACKAGE_GSF.kill())
//                cmds.add("echo 'stopped gsf'")
//            }
//            if (gms == true) {
//                cmds.add(PACKAGE_GMS.kill())
//                cmds.add("echo 'stopped gms'")
//                Thread.sleep(1000)
//            }
//            if (sms == true) {
//                cmds.add(PACKAGE_MESSAGING.kill())
//                cmds.add("echo 'stopped sms'")
//            }
//            Utils.runAsRoot(*cmds.toTypedArray<String>())
//        } catch (e: Exception) {
//            e.printStackTrace()
//        }
//    }

    @JvmStatic
    suspend fun suspendPackage(vararg packages: String) {
        packages.forEach {
            shellRun(
                it.suspend(),
                it.kill()
            )
            delay(1000)
        }
    }

    @JvmStatic
    suspend fun resumePackage(vararg packages: String) {
        packages.forEach {
            shellRun(
                it.resume(),
                it.kill()
            )
            delay(1000)
        }
    }

    fun sqlite3path(): String {
        val context = Utils.getContext()
        val dataDir = ContextCompat.getDataDir(context)
        val binDir = File(dataDir, "bin")
        val dbDir = File(dataDir, "providerDB")
        if (!binDir.exists()) {
            Utils.copyAssetFolder(context.assets, "bin", binDir.path)
            Utils.copyAssetFolder(context.assets, "providerDB", dbDir.path)
            binDir.listFiles()?.forEach {
                it.setExecutable(true)
            }
        }
        Log.i("Modifier", "arch: " + Build.SUPPORTED_ABIS.joinToString(", "))
        val file = File(dataDir, "bin/sqlite3.arm")
        file.setExecutable(true)
        return file.path
    }

    fun copyDB(): File {
        val context = Utils.getContext()
        val dataDir = ContextCompat.getDataDir(context)
        val dbDir = File(dataDir, "providerDB")
        if (!dbDir.exists()) {
            Utils.copyAssetFolder(context.assets, "providerDB", dbDir.path)
        }
        return dbDir
    }

    @JvmStatic
    suspend fun clearConv() {
        val context = Utils.getContext()
        if (context.getSharedPreferences("settings", Context.MODE_PRIVATE)
                .getBoolean("do_not_clean", false)
        ) {
            return
        }
        val model = Build.MODEL.replace(" ", "_")
        if (!(model == "SM-F707N" || model == "SM-F711B" || model == "Pixel_5")) {
            return
        }
        try {
            val sqlite3 = sqlite3path()
            suspendPackage(PACKAGE_MESSAGING)
            val dataDir = ContextCompat.getDataDir(context)
            val providerDir = File(dataDir, "telephony_provider/$model")
            if (!providerDir.exists()) {
                Utils.copyAssetFolder(context.assets, "telephony_provider/$model", providerDir.path)
            }

            val cmds = mutableListOf<String>()
            fun copyToData(file: File) {
                val relative = file.path.replace(providerDir.path, "")
                if (file.isDirectory) {
                    file.listFiles()?.forEach {
                        copyToData(it)
                    }
                } else {
                    val dest = File("/data/data/com.android.providers.telephony/$relative")
                    cmds.addAll(
                        listOf(
                            "cp -f ${file.path} ${dest.path}",
                            "chmod 660 ${dest.path}",
                            "chown radio:radio ${dest.path}"
                        )
                    )
                }
            }
            copyToData(providerDir)
            shellRun(
                "$sqlite3 /data/data/com.google.android.apps.messaging/databases/bugle_db \"DELETE FROM conversations;\"",
                "$sqlite3 /data/data/com.google.android.apps.messaging/databases/bugle_db \"DELETE FROM messages;\"",
                *cmds.toTypedArray(),
            )
            resumePackage(PACKAGE_MESSAGING)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    @JvmStatic
    suspend fun resetAll() {
        try {
            clearConv()
            shellRun(
                PACKAGE_MESSAGING.suspend(),
                PACKAGE_MESSAGING.kill(),
                PACKAGE_MESSAGING.clear(),
                PACKAGE_GSF.clear(),
                PACKAGE_GMS.clear(),
                "sleep 1",
                CMD_START_PLAY_STORE,
                "sleep 1",
                CMD_HOME,
                "sleep 10",
                PACKAGE_GMS.clear(),
                PACKAGE_MESSAGING.resume(),
                CMD_MESSAGING_APP
            )
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    @JvmStatic
    suspend fun killPhoneProcess(force: Boolean = false): Boolean {
        try {
            if (!force) {
                if (shellRun("getprop phonekilled").component1().contains("yes")) {
                    return true
                }
            }
            run kill@{
                repeat(3) {
                    val pid = shellRun("pidof com.android.phone").component1().trim()
                    if (!Regex("[0-9]+").matches(pid)) {
                        Log.e(TAG, "killPhoneProcess: pid not found")
                        return true
                    }
                    Log.i(TAG, "killPhoneProcess: pid=$pid")
                    shellRun("kill -9 $pid")
                    delay(1000)

                    val pidNew = shellRun("pidof com.android.phone").component1().trim()
                    if (pidNew == pid) {
                        Log.e(TAG, "killPhoneProcess: failed to kill phone process")
                    } else {
                        Log.i(TAG, "killPhoneProcess: success, new pid: $pidNew")
                        shellRun("kill -9 $pid", "setprop phonekilled yes")
                        return true
                    }
                }
            }
        } catch (e: Exception) {
            Log.e(TAG, "Error Kill Phone", e)
        }
        return false
    }

    @JvmStatic
    fun sendSmsIntent(sender: String, msg: String) {
        val intent = Intent()
        intent.setAction("com.example.modifier.sms")
        intent.putExtra("sender", sender)
        intent.putExtra(
            "message",
            msg
        )
        val context = Utils.getContext()
        context.sendBroadcast(intent)
    }

    @SuppressLint("DefaultLocale")
    @JvmStatic
    fun genICCID(mnc: String, areaCode: String): String {
        val prefix = String.format("89%02d%s", areaCode.toInt(), mnc)
        return prefix + RandomStringUtils.randomNumeric(20 - prefix.length)
    }

    @JvmStatic
    suspend fun rebooted(): Boolean {
        if (shellRun("getprop rebooted").component1().contains("yes")) {
            return false
        }
        shellRun("setprop rebooted yes")
        return true
    }

    @SuppressLint("SdCardPath")
    @JvmStatic
    suspend fun backup(
        backupItemDao: BackupItemDao,
        type: String,
        sendCount: Int,
        fresh: Boolean = false
    ): BackupItem {
        clearConv()
        delay(3000)
//        ModifierService.instance!!.toggleRcsSwitch(false)
        val context = Utils.getContext()
        val dest = File(
            ContextCompat.getExternalFilesDirs(context, "backup")[0],
            System.currentTimeMillis().toString()
        )
        dest.mkdirs()
        val file = File(ContextCompat.getDataDir(context), "config.json")
        if (!file.exists()) {
            throw Exception("Config file not found")
        }
        withContext(Dispatchers.IO) {
            IOUtils.copy(
                Files.newInputStream(file.toPath()),
                Files.newOutputStream(File(dest, "config.json").toPath())
            )
        }

        val packages = mutableListOf(
            PACKAGE_MESSAGING,
            PACKAGE_GMS,
            PACKAGE_GSF
        )
        packages.forEach {
            File(dest, it).mkdirs()
        }

        val cmds = mutableListOf<String>()

        for (pkg in packages) {
            if (!shellRun("ls /data/data/$pkg").component2()
                    .contains("No such file or directory")
            ) {
                cmds.add("tar zcpf $dest/$pkg/data.tar.gz -C /data/data $pkg ")
            }
            if (!shellRun("ls /data/user_de/0/$pkg").component2()
                    .contains("No such file or directory")
            ) {
                cmds.add("tar zcpf $dest/$pkg/data_de.tar.gz -C /data/user_de/0 $pkg ")
            }
            if (!shellRun("ls /sdcard/Android/data/$pkg").component2()
                    .contains("No such file or directory")
            ) {
                cmds.add("tar zcpf $dest/$pkg/data_ext.tar.gz -C /sdcard/Android/data $pkg ")
            }
        }

        shellRun(*cmds.toTypedArray())

        val backup = BackupItem(
            createdAt = Date().time,
            number = telephonyConfig.number,
            code = telephonyConfig.areaCode,
            country = telephonyConfig.country,
            mcc = telephonyConfig.mcc,
            mnc = telephonyConfig.mnc,
            imei = telephonyConfig.imei,
            imsi = telephonyConfig.imsi,
            iccid = telephonyConfig.iccid,
            path = dest.path,
            sendCount = sendCount,
            lastUse = Date().time,
            type = type,
            fresh = fresh,
            carrierId = Optional.ofNullable(telephonyConfig.carrierId).orElse("1"),
            carrierName = Optional.ofNullable(telephonyConfig.carrierName).orElse("T-Mobile")
        )

        backupItemDao.findBackupForNumber(telephonyConfig.country, telephonyConfig.number)?.let {
            File(it.path).deleteRecursively()
            backupItemDao.delete(it)
            backup.sendCount += it.sendCount
            backup.fresh = false
        }

        backup.id = backupItemDao.insert(backup).toInt()
        return backup
    }

    @SuppressLint("SdCardPath")
    @JvmStatic
    suspend fun restore(backup: BackupItem): Boolean {
        save(
            TelephonyConfig(
                number = backup.number,
                mcc = backup.mcc,
                mnc = backup.mnc,
                country = backup.country,
                areaCode = backup.code,
                iccid = backup.iccid,
                imei = backup.imei,
                imsi = backup.imsi,
                available = true,
                carrierId = backup.carrierId,
                carrierName = backup.carrierName
            ), false
        )
        val packages = mutableListOf(
            PACKAGE_MESSAGING,
            PACKAGE_GMS,
            PACKAGE_GSF
        )

        shellRun(
            PACKAGE_MESSAGING.disable(),
            PACKAGE_GMS.disable(),
            PACKAGE_GSF.disable()
        )
        for (pkg in packages) {
            for (item in listOf("data.tar.gz", "data_de.tar.gz", "data_ext.tar.gz")) {
                val file = File("${backup.path}/$pkg/$item")
                if (!file.exists()) {
                    continue
                }
                val dest = when (item) {
                    "data.tar.gz" -> "/data/data"
                    "data_de.tar.gz" -> "/data/user_de/0"
                    "data_ext.tar.gz" -> "/sdcard/Android/data"
                    else -> ""
                }
                shellRun(
                    "find $dest/$pkg -type f -delete",
                    "tar zxf ${file.path} -C $dest --exclude \"**/AndroidPlatformServices.apk\""
                )
            }
        }
        shellRun(
//            "pm clear com.google.android.gsf",
            PACKAGE_GSF.enable(),
            "sleep 1",
//            "pm clear com.google.android.gms",
            PACKAGE_GMS.enable(),
//            "sleep 30",
            PACKAGE_MESSAGING.enable(),
            "sleep 3",
            CMD_MESSAGING_APP,
            "sleep 5",
            "am start -a android.intent.action.SENDTO -d sms:+18583199738 --es sms_body \"Test\" --ez exit_on_sent false"
        )

        return true
    }

    @JvmStatic
    suspend fun optimize() {
        val context = Utils.getContext()
        val packageManager = context.packageManager
        val info = packageManager.getApplicationInfo("com.google.android.gms", 0)

        shellRun(
            "dumpsys deviceidle whitelist +com.google.android.apps.messaging",
            "dumpsys deviceidle whitelist +${BuildConfig.APPLICATION_ID}",
            "cmd netpolicy add restrict-background-blacklist ${info.uid}",
            "settings put global window_animation_scale 0",
            "settings put global transition_animation_scale 0",
            "settings put global animator_duration_scale 0"
        )
    }

    @JvmStatic
    suspend fun setupSystem() {
        syncTime()
        optimize()
    }

    @JvmStatic
    fun isClashInstalled(context: Context): Boolean {
        val packageManager = context.packageManager
        return try {
            packageManager.getPackageInfo("com.github.metacubex.clash.meta", 0)
            true
        } catch (e: Exception) {
            false
        }
    }

    @JvmStatic
    fun stopClash() {
        try {
            val context = Utils.getContext()
            val intent = Intent()
            intent.action = "com.github.metacubex.clash.meta.action.STOP_CLASH"
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            context.startActivity(intent)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    @JvmStatic
    fun changeClashProfile(name: String, base64: String) {
        try {
            val context = Utils.getContext()
            val intent = Intent()
            intent.action = "com.github.metacubex.clash.meta.action.USE_PROFILE"
            intent.putExtra("base64", base64)
            intent.putExtra("name", name)
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            context.startActivity(intent)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    @JvmStatic
    fun restartModifier() {
        val context = Utils.getContext()
        val mStartActivity: Intent = Intent(context, MainActivity::class.java)
        val mPendingIntentId = 123456
        val mPendingIntent = PendingIntent.getActivity(
            context,
            mPendingIntentId,
            mStartActivity,
            PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
        val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
        mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent)
        System.exit(0)
    }
}
