package com.gh.common.xapk

import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.Settings
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.documentfile.provider.DocumentFile
import com.gh.common.constant.Config
import com.gh.common.util.DirectUtils
import com.gh.common.util.DownloadNotificationHelper
import com.gh.common.util.PackageInstaller
import com.gh.download.DownloadDataHelper
import com.gh.download.DownloadManager
import com.gh.gamecenter.R
import com.gh.gamecenter.common.constant.Constants
import com.gh.gamecenter.common.utils.*
import com.gh.gamecenter.core.AppExecutor
import com.gh.gamecenter.core.utils.ToastUtils
import com.gh.gamecenter.feature.utils.SentryHelper
import com.gh.gamecenter.xapk.XApkUnZipper
import com.gh.gamecenter.xapk.core.XApkFile
import com.gh.gamecenter.xapk.core.XApkUnZipCallback
import com.gh.gamecenter.xapk.core.XApkUnZipEntry
import com.gh.gamecenter.xapk.core.XApkUnZipOutputFactory
import com.gh.gamecenter.xapk.io.NonSplitApksOutput
import com.gh.gamecenter.xapk.io.OBBDocOutput
import com.gh.gamecenter.xapk.io.OBBFileOutput
import com.gh.gamecenter.xapk.io.SplitApksOutput
import com.gh.gamecenter.xapk.io.XApkFileOutput
import com.gh.gamecenter.xapk.pi.IPackageInstaller
import com.gh.ndownload.NDataChanger
import com.halo.assistant.HaloApp
import com.lightgame.download.DownloadEntity
import com.lightgame.utils.Utils
import java.io.File
import java.util.*
import androidx.core.net.toUri
import com.gh.gamecenter.core.utils.SPUtils

/**
 * 目前已知的Xapk内容是:只有一个apk包和一个或做多个obb数据包(多余文件不解压,如果存在多个apk包,则以下安装无效)
 *
 * 每次安装都会重新跑一遍解压安装流程
 * 目前检验是否已解压的根据是存在相同文件名且大小一致(暂时想不到校验MD5值的方法)
 *
 * obb文件直接解压至根目录即可,无需操作文件
 * apk文件这解压的gh-files文件夹中
 *
 * update: 2025/4/29
 * 简单来说，有两种XAPK文件的格式：
 * XAPK = Android App Bundle（基础APK + 配置APK) 【downloadentity.format == xapk(apks)】
 * XAPK = APK文件 + OBB数据文件 【downloadentity.format == xapk】
 *
 */
@SuppressLint("StaticFieldLeak")
object XapkInstaller : XApkUnZipCallback, XApkUnZipOutputFactory {

    const val XAPK_EXTENSION_NAME = "xapk"

    // 通过解压过程存放于 DownloadEntity meta
    const val XAPK_UNZIP_PERCENT = "unzip_percent"
    const val XAPK_UNZIP_STATUS = "unzip_status"

    const val XAPK_DATA_EXTENSION_NAME = "obb"
    const val PACKAGE_EXTENSION_NAME = "apk"

    // 系统是否有 obb 访问漏洞
    val systemHasFlaw by lazy {
        val systemMatched = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
        val rootPath = Environment.getExternalStorageDirectory().path
        val fileListMatched = File(rootPath, "Android").list().contentEquals(File(rootPath, "\u200bAndroid").list())

        systemMatched && fileListMatched
    }

    // 是否使用 DocumentUi 的方式访问 obb 文件夹
    private var useDocStyleToUnzip = false
    private var tempLauncher: ActivityResultLauncher<Uri>? = null

    private const val GUIDE_TYPE_MIUI_OPTIMIZATION = "miui_optimization"
    private const val MIUI_OPTIMIZATION_WARNING_DIALOG_ENTRANCE = "MIUI优化关闭提示弹窗"

    private var mContext = HaloApp.getInstance().application.applicationContext

    private val mXApkUnZipper = XApkUnZipper(this, useFlawToUnzip = systemHasFlaw)
        .also {
            it.registerCallback(this)
        }

    private val mDownloadEntityMap = Collections.synchronizedMap(HashMap<String, DownloadEntity>())

    private val mPendingSessionInfoMap = HashMap<String, XapkPendingSessionInfo>()

    fun isXapk(downloadEntity: DownloadEntity) = XAPK_EXTENSION_NAME == downloadEntity.path.getExtension()

    private fun isXapkApks(downloadEntity: DownloadEntity) = downloadEntity.format == Constants.XAPK_APKS_FORMAT

    /**
     * precondition: assert_true(isXapk())
     *
     */
    // 按并行解压
    @JvmStatic
    fun install(context: Context, downloadEntity: DownloadEntity, showUnzipToast: Boolean = false) {
        this.mContext = context

        val isXapkApks = isXapkApks(downloadEntity)

        if (isXapkApks && MiuiUtils.isMiui() && !MiuiUtils.isMiuiOptimizationDisabled()) {
            // 小米手机开启miui以后，需要引导用户关闭miui优化
            DialogHelper.showMiuiOptimizationWarning(
                context,
                downloadEntity,
                onHintClick = {
                    val guides = Config.getNewApiSettingsEntity()?.install
                    val miuiOptimizationGuide = guides?.guides?.findLast {
                        it.type == GUIDE_TYPE_MIUI_OPTIMIZATION
                    }
                    if (miuiOptimizationGuide != null) {
                        DirectUtils.directToLinkPage(
                            context,
                            miuiOptimizationGuide.link,
                            MIUI_OPTIMIZATION_WARNING_DIALOG_ENTRANCE,
                            ""
                        )
                    }
                },
                onConfirmClick = {
                    if (SystemUtils.isDevelopmentSettingsEnabled(context)) {
                        context.startActivity(
                            Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS)
                                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                        )
                        it.markDismissByTouchInside()
                        it.dismiss()
                    } else {
                        ToastUtils.showToast(context.getString(R.string.miui_open_adb_hint))
                    }
                }
            )
            return
        }

        PermissionHelper.checkManageAllFilesOrStoragePermissionBeforeAction(context) {
            val unzipAction = {
                DownloadManager.getInstance().getDownloadEntitySnapshot(downloadEntity.url, downloadEntity.gameId)
                    ?.let {
                        unzipXapkFile(it, isXapkApks)
                        if (showUnzipToast) {
                            Utils.toast(mContext, "解压过程请勿退出光环助手！")
                        }
                    }
            }

            // XAPK (apks) 格式，不在乎 obb 文件夹是否可读
            if (isXapkApks) {
                unzipAction.invoke()
                return@checkManageAllFilesOrStoragePermissionBeforeAction
            }

            // 以 file 的方式访问 obb 文件夹是否可行
            val isFileStyleObbFolderReadable =
                if (systemHasFlaw) {
                    File(Environment.getExternalStorageDirectory().path, "\u200bAndroid/obb").list() != null
                } else {
                    File(Environment.getExternalStorageDirectory().path, "Android/obb").list() != null
                }

            // 如果是文件夹风格的 obb 文件夹可读，或当前上下文不是 AppCompatActivity，或当前上下文已经被销毁，则直接解压
            if (isFileStyleObbFolderReadable
                || context !is AppCompatActivity
                || context.isFinishing
            ) {
                unzipAction.invoke()
            } else {
                unzipWithCulpritHandled(context, downloadEntity.url, unzipAction)
            }
        }
    }

    /**
     * 兜底解压方案，适用于一些奇葩系统
     */
    private fun unzipWithCulpritHandled(activity: AppCompatActivity, xApkUrl: String, unzipAction: () -> Unit?) {
        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R
            || Build.VERSION.SDK_INT == Build.VERSION_CODES.S
        ) {
            val isDocStyleObbFolderReadable = isDocStyleObbFolderReadable(activity)

            if (isDocStyleObbFolderReadable) {
                useDocStyleToUnzip = true
                unzipAction.invoke()
            } else {
                DialogHelper.showDialog(
                    context = activity,
                    title = "安装提示",
                    content = "为了安装 XAPK 文件，请授予 OBB 文件夹的访问权限",
                    confirmText = "确定",
                    cancelText = "取消",
                    confirmClickCallback = {
                        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
                        intent.setFlags(
                            Intent.FLAG_GRANT_READ_URI_PERMISSION
                                    and Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                                    and Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
                                    and Intent.FLAG_GRANT_PREFIX_URI_PERMISSION
                        )

                        val obbUri = OBBDocOutput.ANDROID_OBB_DOC_STYLE_URI.toUri()
                        val df = DocumentFile.fromTreeUri(activity, obbUri)
                        if (df != null) {
                            intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, df.uri)
                        }

                        tempLauncher =
                            activity.registerActivityResultLauncher(ActivityResultContracts.OpenDocumentTree()) { uri ->
                                if (uri != null) {
                                    activity.contentResolver.takePersistableUriPermission(
                                        uri,
                                        Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                                    )

                                    useDocStyleToUnzip = true
                                    unzipAction.invoke()
                                }

                                tempLauncher?.unregister()
                            }

                        tempLauncher?.launch(obbUri)
                    },
                    cancelClickCallback = {
                        unzipAction.invoke()
                    }
                )
            }
        } else {
            PermissionHelper.checkStoragePermissionBeforeAction(activity) {
                // 设备大于 12，需要重启才能生效 （是的，我们又重启了！）
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                    // 记录应用重启前需要重解压的信息
                    SPUtils.setString(Constants.SP_XAPK_UNZIP_ACTIVITY, activity.javaClass.name)
                    SPUtils.setString(Constants.SP_XAPK_URL, xApkUrl)

                    val pm = activity.packageManager
                    val intent = pm.getLaunchIntentForPackage(activity.packageName)
                    val mainIntent = Intent.makeRestartActivityTask(intent!!.component)
                    activity.startActivity(mainIntent)
                    Runtime.getRuntime().exit(0)
                } else {
                    unzipAction.invoke()
                }
            }
        }
    }

    private fun unzipXapkFile(downloadEntity: DownloadEntity, isXapkApks: Boolean) {
        XApkUnZipEntry(downloadEntity.path, File(downloadEntity.path), isXapkApks).let(mXApkUnZipper::unzip)
        mDownloadEntityMap[downloadEntity.path] = downloadEntity
    }

    @JvmStatic
    fun cancelUnzipTask(downloadEntity: DownloadEntity) {
        mXApkUnZipper.cancel(downloadEntity.path)
    }

    override fun onProgress(apk: XApkFile, progress: Float) {
        val downloadEntity = mDownloadEntityMap[apk.file.path] ?: return
        downloadEntity.meta[XAPK_UNZIP_PERCENT] = String.format(Locale.CHINA, "%.2f", progress * 100)
        downloadEntity.meta[XAPK_UNZIP_STATUS] = XapkUnzipStatus.UNZIPPING.name

        AppExecutor.ioExecutor.execute {
            NDataChanger.notifyDataChanged(downloadEntity)
        }

        debugOnly {
            Utils.log("unzip", "onProgress->$progress")
        }
    }

    override fun onSuccess(apk: XApkFile, installer: IPackageInstaller) {
        val downloadEntity = mDownloadEntityMap[apk.file.path] ?: return

        downloadEntity.meta[XAPK_UNZIP_PERCENT] = "100.0"
        downloadEntity.meta[XAPK_UNZIP_STATUS] = XapkUnzipStatus.SUCCESS.name

        AppExecutor.ioExecutor.execute {
            NDataChanger.notifyDataChanged(downloadEntity)
            DownloadManager.getInstance().updateDownloadEntity(downloadEntity)
            DownloadDataHelper.uploadDownloadStatusEvent(downloadEntity, "xapk解压成功")
        }

        debugOnly {
            Utils.log("unzip", "onSuccess->${downloadEntity.path}")
        }

        installer.install(mContext)
    }

    override fun onError(apk: XApkFile, exception: Throwable) {
        val downloadEntity = mDownloadEntityMap[apk.file.path] ?: return
        downloadEntity.meta[XAPK_UNZIP_STATUS] = XapkUnzipStatus.FAILURE.name

        AppExecutor.ioExecutor.execute {
            DownloadNotificationHelper.addOrUpdateDownloadNotification(downloadEntity)
            NDataChanger.notifyDataChanged(downloadEntity)
            DownloadManager.getInstance().updateDownloadEntity(downloadEntity)

            DownloadDataHelper.uploadDownloadStatusEvent(downloadEntity, "xapk解压失败")

            SensorsBridge.trackGameDecompressionFailed(
                downloadEntity.gameId,
                downloadEntity.name,
                downloadEntity.categoryChinese,
                exception.localizedMessage ?: "unknown error"
            )
        }

        debugOnly {
            Utils.log("unzip", "onFailure->$exception")
        }
    }

    override fun onCancel(apk: XApkFile) {
        val downloadEntity = mDownloadEntityMap.remove(apk.file.path) ?: return
        downloadEntity.meta[XAPK_UNZIP_PERCENT] = "0.0"
        downloadEntity.meta[XAPK_UNZIP_STATUS] = XapkUnzipStatus.CANCEL.name
        AppExecutor.ioExecutor.execute {
            NDataChanger.notifyDataChanged(downloadEntity)
            DownloadManager.getInstance().updateDownloadEntity(downloadEntity)
        }
    }

    override fun onNext(apk: XApkFile, fileName: String) {
        debugOnly {
            Utils.log("unzip", "onNext->$fileName")
        }
    }

    fun getPendingSessionInfo(packagePath: String): XapkPendingSessionInfo? = mPendingSessionInfoMap[packagePath]

    fun isInstalling(packagePath: String): Boolean = mPendingSessionInfoMap.containsKey(packagePath)

    /**
     * 通知XAPK安装完成
     */
    fun onInstalled(packagePath: String) {
        val downloadEntity = mDownloadEntityMap.remove(packagePath) ?: return

        mPendingSessionInfoMap.remove(packagePath)

        downloadEntity.meta[XAPK_UNZIP_PERCENT] = "100.0"
        downloadEntity.meta[XAPK_UNZIP_STATUS] = XapkUnzipStatus.INSTALLED.name

        AppExecutor.ioExecutor.execute {
            NDataChanger.notifyDataChanged(downloadEntity)
            DownloadManager.getInstance().updateDownloadEntity(downloadEntity)
            DownloadDataHelper.uploadDownloadStatusEvent(downloadEntity, "xapk安装成功")
        }
    }

    fun onInstallCanceled(packagePath: String) {

        val downloadEntity = mDownloadEntityMap.remove(packagePath) ?: return

        mPendingSessionInfoMap.remove(packagePath)

        downloadEntity.meta[XAPK_UNZIP_PERCENT] = "0.0"
        downloadEntity.meta[XAPK_UNZIP_STATUS] = XapkUnzipStatus.CANCEL.name

        AppExecutor.ioExecutor.execute {
            NDataChanger.notifyDataChanged(downloadEntity)
            DownloadManager.getInstance().updateDownloadEntity(downloadEntity)
            DownloadDataHelper.uploadDownloadStatusEvent(downloadEntity, "xapk安装取消")
        }
    }

    /**
     * 更新当前XAPK安装状态, 主要针对以下出现安装结果回调异常的情况：
     * 1. 用户触碰安装弹窗(原生Android系统)区域外导致安装取消
     */
    fun updateCurrentInstallStatus() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || mPendingSessionInfoMap.isEmpty()) {
            return
        }

        val updateList = mutableListOf<XapkPendingSessionInfo>()

        for (pendingSessionInfoEntry in mPendingSessionInfoMap) {

            val pendingSessionInfo = pendingSessionInfoEntry.value

            if (pendingSessionInfo.getStatus() == XapkPendingSessionInfo.STATUS_PENDING_USER_ACTION) {// 用户触摸安装弹窗外部区域取消安装后，更新安装状态
                val installer = mContext.packageManager.packageInstaller
                val sessionId = pendingSessionInfo.sessionId
                if (sessionId != -1) {
                    val sessionInfo = installer.getSessionInfo(sessionId)
                    // 表示用户点击了安装弹窗外部区域
                    if (sessionInfo == null) {
                        pendingSessionInfo.updateStatus(XapkPendingSessionInfo.STATUS_INSTALL_CANCELED)
                    } else if (sessionInfo.progress <= 0.8F) {
                        AppExecutor.ioExecutor.execute {
                            try {
                                installer.abandonSession(sessionInfo.sessionId)
                            } catch (_: Exception) {
                                // 有概率抛SecurityException，这里只要直接catch不做处理即可
                            }
                        }
                        pendingSessionInfo.updateStatus(XapkPendingSessionInfo.STATUS_INSTALL_CANCELED)
                    }
                }
            }

            val installStatus = pendingSessionInfo.getStatus()
            if (installStatus == XapkPendingSessionInfo.STATUS_INSTALL_CANCELED
                || installStatus == XapkPendingSessionInfo.STATUS_INSTALL_SUCCESS
            ) {
                updateList.add(pendingSessionInfo)
            }
        }

        for (pendingSessionInfo in updateList) {
            val downloadEntity = mDownloadEntityMap[pendingSessionInfo.path] ?: continue
            val installStatus = pendingSessionInfo.getStatus()
            if (installStatus == XapkPendingSessionInfo.STATUS_INSTALL_SUCCESS) {
                onInstalled(downloadEntity.path)
            } else if (installStatus == XapkPendingSessionInfo.STATUS_INSTALL_CANCELED) {
                onInstallCanceled(downloadEntity.path)
            }
        }
    }

    override fun onCreateOBBOutput(apk: XApkFile): XApkFileOutput<Unit> {
        return if (useDocStyleToUnzip) OBBDocOutput(mContext) else OBBFileOutput()
    }

    override fun onCreateApkOutput(apk: XApkFile): XApkFileOutput<IPackageInstaller> {
        val isMultiApks = apk.manifest.isMultiApks
        return if (isMultiApks) {
            SplitApksOutput(mContext.applicationContext) {
                SplitApksInstaller(apk, it)
            }
        } else {
            NonSplitApksOutput {
                NonSplitApkInstaller(apk, it)
            }
        }
    }

    /**
     * 是否能以 DocumentUi 的方式访问 obb 文件夹
     */
    private fun isDocStyleObbFolderReadable(context: Context): Boolean {
        return context
            .contentResolver
            .persistedUriPermissions
            .any { it.uri == OBBDocOutput.ANDROID_OBB_URI.toUri() }
    }

    private class SplitApksInstaller(
        private val xApkFile: XApkFile,
        private val sessionId: Int,
    ) : IPackageInstaller {

        override fun install(context: Context) {
            val applicationContext = context.applicationContext
            val downloadEntity = mDownloadEntityMap[xApkFile.file.path] ?: return

            mPendingSessionInfoMap[downloadEntity.path] = XapkPendingSessionInfo(downloadEntity.path, sessionId)
            AppExecutor.ioExecutor.execute {// 有可能卡顿造成anr
                try {
                    PackageInstaller.installMultiple(
                        applicationContext,
                        downloadEntity.packageName,
                        downloadEntity.path,
                        sessionId
                    )
                    NDataChanger.notifyDataChanged(downloadEntity)
                } catch (ignore: Exception) {
                }
            }
        }
    }

    private class NonSplitApkInstaller(
        private val xApkFile: XApkFile,
        private val file: File
    ) : IPackageInstaller {

        override fun install(context: Context) {
            val downloadEntity = mDownloadEntityMap[xApkFile.file.path] ?: return
            PackageInstaller.install(
                context,
                downloadEntity.isPlugin,
                file.absolutePath,
                downloadEntity
            )
        }
    }
}

enum class XapkUnzipStatus(status: String) {
    UNZIPPING("unzipping"),
    SUCCESS("success"),
    CANCEL("cancel"),
    INSTALLED("installed"),
    FAILURE("failure");
}