/*
 * This file is part of LSPosed.
 *
 * LSPosed is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * LSPosed is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with LSPosed.  If not, see <https://www.gnu.org/licenses/>.
 *
 * Copyright (C) 2021 LSPosed Contributors
 */

package org.lsposed.lspd.service;

import static android.content.pm.ServiceInfo.FLAG_ISOLATED_PROCESS;
import static org.lsposed.lspd.service.ServiceManager.TAG;
import static org.lsposed.lspd.service.ServiceManager.existsInGlobalNamespace;

import android.content.IIntentReceiver;
import android.content.IIntentSender;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.ApplicationInfo;
import android.content.pm.ComponentInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.pm.VersionedPackage;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemProperties;
import android.util.Log;
import android.util.Pair;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.lsposed.lspd.models.Application;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;

import rikka.parcelablelist.ParcelableListSlice;

public class PackageService {

    static final int INSTALL_FAILED_INTERNAL_ERROR = -110;
    static final int INSTALL_REASON_UNKNOWN = 0;
    static final int MATCH_ANY_USER = 0x00400000; // PackageManager.MATCH_ANY_USER

    static final int MATCH_ALL_FLAGS = PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE | PackageManager.MATCH_UNINSTALLED_PACKAGES | MATCH_ANY_USER;
    public static final int PER_USER_RANGE = 100000;

    private static IPackageManager pm = null;
    private static IBinder binder = null;

    static boolean isAlive() {
        var pm = getPackageManager();
        return pm != null && pm.asBinder().isBinderAlive();
    }

    private static final IBinder.DeathRecipient recipient = new IBinder.DeathRecipient() {
        @Override
        public void binderDied() {
            Log.w(TAG, "pm is dead");
            binder.unlinkToDeath(this, 0);
            binder = null;
            pm = null;
        }
    };

    private static IPackageManager getPackageManager() {
        if (binder == null || pm == null) {
            binder = ServiceManager.getService("package");
            if (binder == null) return null;
            try {
                binder.linkToDeath(recipient, 0);
            } catch (RemoteException e) {
                Log.e(TAG, Log.getStackTraceString(e));
            }
            pm = IPackageManager.Stub.asInterface(binder);
        }
        return pm;
    }

    @Nullable
    public static PackageInfo getPackageInfo(String packageName, int flags, int userId) throws RemoteException {
        IPackageManager pm = getPackageManager();
        if (pm == null) return null;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            return pm.getPackageInfo(packageName, (long) flags, userId);
        }
        return pm.getPackageInfo(packageName, flags, userId);
    }

    public static @NonNull
    Map<Integer, PackageInfo> getPackageInfoFromAllUsers(String packageName, int flags) throws RemoteException {
        IPackageManager pm = getPackageManager();
        Map<Integer, PackageInfo> res = new HashMap<>();
        if (pm == null) return res;
        for (var user : UserService.getUsers()) {
            var info = getPackageInfo(packageName, flags, user.id);
            if (info != null && info.applicationInfo != null) res.put(user.id, info);
        }
        return res;
    }

    @Nullable
    public static ApplicationInfo getApplicationInfo(String packageName, int flags, int userId) throws RemoteException {
        IPackageManager pm = getPackageManager();
        if (pm == null) return null;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            return pm.getApplicationInfo(packageName, (long) flags, userId);
        }
        return pm.getApplicationInfo(packageName, flags, userId);
    }

    // Only for manager
    public static ParcelableListSlice<PackageInfo> getInstalledPackagesFromAllUsers(int flags, boolean filterNoProcess) throws RemoteException {
        List<PackageInfo> res = new ArrayList<>();
        IPackageManager pm = getPackageManager();
        if (pm == null) return ParcelableListSlice.emptyList();
        for (var user : UserService.getUsers()) {
            // in case pkginfo of other users in primary user
            ParceledListSlice<PackageInfo> infos;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                infos = pm.getInstalledPackages((long) flags, user.id);
            } else {
                infos = pm.getInstalledPackages(flags, user.id);
            }
            res.addAll(infos
                    .getList().parallelStream()
                    .filter(info -> info.applicationInfo != null && info.applicationInfo.uid / PER_USER_RANGE == user.id)
                    .filter(info -> {
                        try {
                            return isPackageAvailable(info.packageName, user.id, true);
                        } catch (RemoteException e) {
                            return false;
                        }
                    })
                    .collect(Collectors.toList()));
        }
        if (filterNoProcess) {
            return new ParcelableListSlice<>(res.parallelStream().filter(packageInfo -> {
                try {
                    PackageInfo pkgInfo = getPackageInfoWithComponents(packageInfo.packageName, MATCH_ALL_FLAGS, packageInfo.applicationInfo.uid / PER_USER_RANGE);
                    return !fetchProcesses(pkgInfo).isEmpty();
                } catch (RemoteException e) {
                    Log.w(TAG, "filter failed", e);
                    return true;
                }
            }).collect(Collectors.toList()));
        }
        return new ParcelableListSlice<>(res);
    }

    private static Set<String> fetchProcesses(PackageInfo pkgInfo) {
        HashSet<String> processNames = new HashSet<>();
        if (pkgInfo == null) return processNames;
        for (ComponentInfo[] componentInfos : new ComponentInfo[][]{pkgInfo.activities, pkgInfo.receivers, pkgInfo.providers}) {
            if (componentInfos == null) continue;
            for (ComponentInfo componentInfo : componentInfos) {
                processNames.add(componentInfo.processName);
            }
        }
        if (pkgInfo.services == null) return processNames;
        for (ServiceInfo service : pkgInfo.services) {
            if ((service.flags & FLAG_ISOLATED_PROCESS) == 0) {
                processNames.add(service.processName);
            }
        }
        return processNames;
    }

    public static Pair<Set<String>, Integer> fetchProcessesWithUid(Application app) throws RemoteException {
        IPackageManager pm = getPackageManager();
        if (pm == null) return new Pair<>(Collections.emptySet(), -1);
        PackageInfo pkgInfo = getPackageInfoWithComponents(app.packageName, MATCH_ALL_FLAGS, app.userId);
        if (pkgInfo == null || pkgInfo.applicationInfo == null)
            return new Pair<>(Collections.emptySet(), -1);
        return new Pair<>(fetchProcesses(pkgInfo), pkgInfo.applicationInfo.uid);
    }

    public static boolean isPackageAvailable(String packageName, int userId, boolean ignoreHidden) throws RemoteException {
        return pm.isPackageAvailable(packageName, userId) || (ignoreHidden && pm.getApplicationHiddenSettingAsUser(packageName, userId));
    }

    @SuppressWarnings({"ConstantConditions", "SameParameterValue"})
    @Nullable
    private static PackageInfo getPackageInfoWithComponents(String packageName, int flags, int userId) throws RemoteException {
        IPackageManager pm = getPackageManager();
        if (pm == null) return null;
        PackageInfo pkgInfo;
        try {
            pkgInfo = getPackageInfo(packageName, flags | PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES | PackageManager.GET_RECEIVERS | PackageManager.GET_PROVIDERS, userId);
        } catch (Exception e) {
            pkgInfo = getPackageInfo(packageName, flags, userId);
            if (pkgInfo == null) return null;
            try {
                pkgInfo.activities = getPackageInfo(packageName, flags | PackageManager.GET_ACTIVITIES, userId).activities;
            } catch (Exception ignored) {

            }
            try {
                pkgInfo.services = getPackageInfo(packageName, flags | PackageManager.GET_SERVICES, userId).services;
            } catch (Exception ignored) {

            }
            try {
                pkgInfo.receivers = getPackageInfo(packageName, flags | PackageManager.GET_RECEIVERS, userId).receivers;
            } catch (Exception ignored) {

            }
            try {
                pkgInfo.providers = getPackageInfo(packageName, flags | PackageManager.GET_PROVIDERS, userId).providers;
            } catch (Exception ignored) {

            }
        }
        if (pkgInfo == null || pkgInfo.applicationInfo == null || (!pkgInfo.packageName.equals("android") && (pkgInfo.applicationInfo.sourceDir == null || !existsInGlobalNamespace(pkgInfo.applicationInfo.sourceDir) || !isPackageAvailable(packageName, userId, true))))
            return null;
        return pkgInfo;
    }

    static abstract class IntentSenderAdaptor extends IIntentSender.Stub {
        public abstract void send(Intent intent);

        @Override
        public int send(int code, Intent intent, String resolvedType, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
            send(intent);
            return 0;
        }

        @Override
        public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
            send(intent);
        }

        public IntentSender getIntentSender() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
            @SuppressWarnings("JavaReflectionMemberAccess")
            Constructor<IntentSender> intentSenderConstructor = IntentSender.class.getConstructor(IIntentSender.class);
            intentSenderConstructor.setAccessible(true);
            return intentSenderConstructor.newInstance(this);
        }
    }

    public static boolean uninstallPackage(VersionedPackage versionedPackage, int userId) throws RemoteException, InterruptedException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
        CountDownLatch latch = new CountDownLatch(1);
        final boolean[] result = {false};
        var flag = userId == -1 ? 0x00000002 : 0; //PackageManager.DELETE_ALL_USERS = 0x00000002; UserHandle ALL = new UserHandle(-1);
        pm.getPackageInstaller().uninstall(versionedPackage, "android", flag, new IntentSenderAdaptor() {
            @Override
            public void send(Intent intent) {
                int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE);
                result[0] = status == PackageInstaller.STATUS_SUCCESS;
                Log.d(TAG, intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE));
                latch.countDown();
            }
        }.getIntentSender(), userId == -1 ? 0 : userId);
        latch.await();
        return result[0];
    }

    public static int installExistingPackageAsUser(String packageName, int userId) throws RemoteException {
        IPackageManager pm = getPackageManager();
        Log.d(TAG, "about to install existing package " + packageName + "/" + userId);
        if (pm == null) return INSTALL_FAILED_INTERNAL_ERROR;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            return pm.installExistingPackageAsUser(packageName, userId, 0, INSTALL_REASON_UNKNOWN, null);
        } else {
            return pm.installExistingPackageAsUser(packageName, userId, 0, INSTALL_REASON_UNKNOWN);
        }
    }

    @Nullable
    public static ParcelableListSlice<ResolveInfo> queryIntentActivities(Intent intent, String resolvedType, int flags, int userId) {
        try {
            IPackageManager pm = getPackageManager();
            if (pm == null) return null;
            ParceledListSlice<ResolveInfo> infos;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                infos = pm.queryIntentActivities(intent, resolvedType, (long) flags, userId);
            } else {
                infos = pm.queryIntentActivities(intent, resolvedType, flags, userId);
            }
            return new ParcelableListSlice<>(infos.getList());
        } catch (Exception e) {
            Log.e(TAG, "queryIntentActivities", e);
            return new ParcelableListSlice<>(new ArrayList<>());
        }
    }

    @Nullable
    public static Intent getLaunchIntentForPackage(String packageName) throws RemoteException {
        Intent intentToResolve = new Intent(Intent.ACTION_MAIN);
        intentToResolve.addCategory(Intent.CATEGORY_INFO);
        intentToResolve.setPackage(packageName);
        var ris = queryIntentActivities(intentToResolve, intentToResolve.getType(), 0, 0);

        // Otherwise, try to find a main launcher activity.
        if (ris == null || ris.getList().size() == 0) {
            // reuse the intent instance
            intentToResolve.removeCategory(Intent.CATEGORY_INFO);
            intentToResolve.addCategory(Intent.CATEGORY_LAUNCHER);
            intentToResolve.setPackage(packageName);
            ris = queryIntentActivities(intentToResolve, intentToResolve.getType(), 0, 0);
        }
        if (ris == null || ris.getList().size() == 0) {
            return null;
        }
        Intent intent = new Intent(intentToResolve);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.setClassName(ris.getList().get(0).activityInfo.packageName,
                ris.getList().get(0).activityInfo.name);
        return intent;
    }

    public static void clearApplicationProfileData(String packageName) throws RemoteException {
        IPackageManager pm = getPackageManager();
        if (pm == null) return;
        pm.clearApplicationProfileData(packageName);
    }

    public static boolean performDexOptMode(String packageName) throws RemoteException {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
            Process process = null;
            try {
                // The 'speed-profile' filter is a balanced choice for performance.
                String command = "cmd package compile -m speed-profile -f " + packageName;
                process = Runtime.getRuntime().exec(command);

                // Capture and log the output for debugging.
                StringBuilder output = new StringBuilder();
                try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                    String line;
                    while ((line = reader.readLine()) != null) {
                        output.append(line).append("\n");
                    }
                }

                int exitCode = process.waitFor();
                Log.i(TAG, "Dexopt command finished for " + packageName + " with exit code: " + exitCode);

                // A successful command returns exit code 0 and typically "Success" in its output.
                return exitCode == 0 && output.toString().contains("Success");

            } catch (Exception e) {
                Log.e(TAG, "Failed to execute dexopt shell command for " + packageName, e);
                if (e instanceof InterruptedException) {
                    // Preserve the interrupted status.
                    Thread.currentThread().interrupt();
                }
                return false;
            } finally {
                if (process != null) {
                    process.destroy();
                }
            }
        } else {
            // Fallback to the original reflection method for older Android versions.
            IPackageManager pm = getPackageManager();
            if (pm == null) return false;
            return pm.performDexOptMode(packageName,
                SystemProperties.getBoolean("dalvik.vm.usejitprofiles", false),
                SystemProperties.get("pm.dexopt.install", "speed-profile"), true, true, null);
        }
    }
}
