/*
 * 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.Intent.EXTRA_UID;
import static org.lsposed.lspd.service.LSPNotificationManager.SCOPE_CHANNEL_ID;
import static org.lsposed.lspd.service.LSPNotificationManager.UPDATED_CHANNEL_ID;
import static org.lsposed.lspd.service.PackageService.PER_USER_RANGE;
import static org.lsposed.lspd.service.ServiceManager.TAG;
import static org.lsposed.lspd.service.ServiceManager.getExecutorService;

import android.app.IApplicationThread;
import android.app.IUidObserver;
import android.content.Context;
import android.content.IIntentReceiver;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.provider.Telephony;
import android.telephony.TelephonyManager;
import android.util.Log;

import org.lsposed.daemon.BuildConfig;
import org.lsposed.lspd.models.Application;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.zip.ZipFile;

import hidden.HiddenApiBridge;
import io.github.libxposed.service.IXposedScopeCallback;

public class LSPosedService extends ILSPosedService.Stub {
    private static final int AID_NOBODY = 9999;
    private static final int USER_NULL = -10000;
    private static final String ACTION_USER_ADDED = "android.intent.action.USER_ADDED";
    public static final String ACTION_USER_REMOVED = "android.intent.action.USER_REMOVED";
    private static final String EXTRA_USER_HANDLE = "android.intent.extra.user_handle";
    private static final String EXTRA_REMOVED_FOR_ALL_USERS = "android.intent.extra.REMOVED_FOR_ALL_USERS";
    private static boolean bootCompleted = false;
    private IBinder appThread = null;

    private static boolean isModernModules(ApplicationInfo info) {
        String[] apks;
        if (info.splitSourceDirs != null) {
            apks = Arrays.copyOf(info.splitSourceDirs, info.splitSourceDirs.length + 1);
            apks[info.splitSourceDirs.length] = info.sourceDir;
        } else apks = new String[]{info.sourceDir};
        for (var apk : apks) {
            try (var zip = new ZipFile(apk)) {
                if (zip.getEntry("META-INF/xposed/java_init.list") != null) {
                    return true;
                }
            } catch (IOException ignored) {
            }
        }
        return false;
    }

    @Override
    public ILSPApplicationService requestApplicationService(int uid, int pid, String processName, IBinder heartBeat) {
        if (Binder.getCallingUid() != 1000) {
            Log.w(TAG, "Someone else got my binder!?");
            return null;
        }
        if (ServiceManager.getApplicationService().hasRegister(uid, pid)) {
            Log.d(TAG, "Skipped duplicated request for uid " + uid + " pid " + pid);
            return null;
        }
        if (!ServiceManager.getManagerService().shouldStartManager(pid, uid, processName) && ConfigManager.getInstance().shouldSkipProcess(new ConfigManager.ProcessScope(processName, uid))) {
            Log.d(TAG, "Skipped " + processName + "/" + uid);
            return null;
        }
        Log.d(TAG, "returned service");
        return ServiceManager.requestApplicationService(uid, pid, processName, heartBeat);
    }

    /**
     * This part is quite complex.
     * For modules, we never care about its user id, we only care about its apk path.
     * So we will only process module's removal when it's removed from all users.
     * And FULLY_REMOVE is exactly the one.
     * <p>
     * For applications, we care about its user id.
     * So we will process application's removal when it's removed from every single user.
     * However, PACKAGE_REMOVED will be triggered by `pm hide`, so we use UID_REMOVED instead.
     */

    private void dispatchPackageChanged(Intent intent) {
        if (intent == null) return;
        int uid = intent.getIntExtra(EXTRA_UID, AID_NOBODY);
        if (uid == AID_NOBODY || uid <= 0) return;
        int userId = intent.getIntExtra("android.intent.extra.user_handle", USER_NULL);
        var intentAction = intent.getAction();
        if (intentAction == null) return;
        var allUsers = intent.getBooleanExtra(EXTRA_REMOVED_FOR_ALL_USERS, false);
        if (userId == USER_NULL) userId = uid % PER_USER_RANGE;
        Uri uri = intent.getData();
        var module = ConfigManager.getInstance().getModule(uid);
        String moduleName = (uri != null) ? uri.getSchemeSpecificPart() : (module != null) ? module.packageName : null;

        ApplicationInfo applicationInfo = null;
        if (moduleName != null) {
            try {
                applicationInfo = PackageService.getApplicationInfo(moduleName, PackageManager.GET_META_DATA | PackageService.MATCH_ALL_FLAGS, 0);
            } catch (Throwable ignored) {
            }
        }

        boolean isXposedModule = applicationInfo != null && ((applicationInfo.metaData != null && applicationInfo.metaData.containsKey("xposedminversion")) || isModernModules(applicationInfo));

        switch (intentAction) {
            case Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
                // for module, remove module
                // because we only care about when the apk is gone
                if (moduleName != null) {
                    if (allUsers && ConfigManager.getInstance().removeModule(moduleName)) {
                        isXposedModule = true;
                        broadcastAndShowNotification(moduleName, userId, intent, true);
                    }
                    LSPNotificationManager.cancelNotification(UPDATED_CHANNEL_ID, moduleName, userId);
                }
            }
            case Intent.ACTION_PACKAGE_REMOVED -> {
                if (moduleName != null) {
                    LSPNotificationManager.cancelNotification(UPDATED_CHANNEL_ID, moduleName, userId);
                }
                break;
            }
            case Intent.ACTION_PACKAGE_ADDED, Intent.ACTION_PACKAGE_CHANGED -> {
                var configManager = ConfigManager.getInstance();
                // make sure that the change is for the complete package, not only a
                // component
                String[] components = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST);
                if (components != null && !Arrays.stream(components).reduce(false, (p, c) -> p || c.equals(moduleName), Boolean::logicalOr)) {
                    return;
                }
                if (isXposedModule) {
                    // When installing a new Xposed module, we update the apk path to mark it as a
                    // module to send a broadcast when modules that have not been activated are
                    // uninstalled.
                    // If cache not updated, assume it's not xposed module
                    isXposedModule = configManager.updateModuleApkPath(moduleName, ConfigManager.getInstance().getModuleApkPath(applicationInfo), false);
                } else {
                    if (configManager.isUidHooked(uid)) {
                        // it will automatically remove obsolete app from database
                        configManager.updateAppCache();
                    }
                    if (intentAction.equals(Intent.ACTION_PACKAGE_ADDED) && !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
                        for (String xposedModule : configManager.getAutoIncludeModules()) {
                            // For Xposed modules with auto_include set, we always add new applications
                            // to its scope
                            var list = configManager.getModuleScope(xposedModule);
                            if (list != null) {
                                Application scope = new Application();
                                scope.packageName = moduleName;
                                scope.userId = userId;
                                list.add(scope);
                                try {
                                    if (!configManager.setModuleScope(xposedModule, list)) {
                                        Log.e(TAG, "failed to set scope for " + xposedModule);
                                    }
                                } catch(RemoteException re) {
                                    Log.e(TAG, "failed to set scope for " + xposedModule, re);
                                }
                            }
                        }
                    }
                }
                broadcastAndShowNotification(moduleName, userId, intent, isXposedModule);
            }
            case Intent.ACTION_UID_REMOVED -> {
                // when a package is removed (rather than hide) for a single user
                // (apk may still be there because of multi-user)
                broadcastAndShowNotification(moduleName, userId, intent, isXposedModule);
                if (isXposedModule) {
                    // it will auto remove obsolete app and scope from database
                    ConfigManager.getInstance().updateCache();
                } else if (ConfigManager.getInstance().isUidHooked(uid)) {
                    // it will auto remove obsolete scope from database
                    ConfigManager.getInstance().updateAppCache();
                }
            }
        }
        boolean removed = Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(intentAction) || Intent.ACTION_UID_REMOVED.equals(intentAction);

        Log.d(TAG, "Package changed: uid=" + uid + " userId=" + userId + " action=" + intentAction + " isXposedModule=" + isXposedModule + " isAllUsers=" + allUsers);

        if (BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME.equals(moduleName) && userId == 0) {
            Log.d(TAG, "Manager updated");
            ConfigManager.getInstance().updateManager(removed);
        }
    }

    private void broadcastAndShowNotification(String packageName, int userId, Intent intent, boolean isXposedModule) {
        Log.d(TAG, "package " + packageName + " changed, dispatching to manager");
        var action = intent.getAction();
        var allUsers = intent.getBooleanExtra(EXTRA_REMOVED_FOR_ALL_USERS, false);
        intent.putExtra("android.intent.extra.PACKAGES", packageName);
        intent.putExtra(Intent.EXTRA_USER, userId);
        intent.putExtra("isXposedModule", isXposedModule);
        LSPManagerService.broadcastIntent(intent);
        if (isXposedModule) {
            var enabledModules = ConfigManager.getInstance().enabledModules();
            var scope = ConfigManager.getInstance().getModuleScope(packageName);
            boolean systemModule = scope != null && scope.parallelStream().anyMatch(app -> app.packageName.equals("system"));
            boolean enabled = Arrays.asList(enabledModules).contains(packageName);
            if (!(Intent.ACTION_UID_REMOVED.equals(action) || Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action) || allUsers))
                LSPNotificationManager.notifyModuleUpdated(packageName, userId, enabled, systemModule);
        }
    }

    private void dispatchUserChanged(Intent intent) {
        if (intent == null) return;
        int uid = intent.getIntExtra(EXTRA_USER_HANDLE, AID_NOBODY);
        if (uid == AID_NOBODY || uid <= 0) return;
        LSPManagerService.broadcastIntent(intent);
    }

    private void dispatchBootCompleted(Intent intent) {
        bootCompleted = true;
        var configManager = ConfigManager.getInstance();
        if (configManager.enableStatusNotification()) {
            LSPNotificationManager.notifyStatusNotification();
        } else {
            LSPNotificationManager.cancelStatusNotification();
        }
    }

    private void dispatchConfigurationChanged(Intent intent) {
        if (!bootCompleted) return;
        ConfigFileManager.reloadConfiguration();
        var configManager = ConfigManager.getInstance();
        if (configManager.enableStatusNotification()) {
            LSPNotificationManager.notifyStatusNotification();
        } else {
            LSPNotificationManager.cancelStatusNotification();
        }
    }

    private void dispatchSecretCodeReceive(Intent i) {
        LSPManagerService.openManager(null);
    }

    private void dispatchOpenManager(Intent intent) {
        LSPManagerService.openManager(intent.getData());
    }

    private void dispatchModuleScope(Intent intent) {
        Log.d(TAG, "dispatchModuleScope: " + intent);
        var data = intent.getData();
        var extras = intent.getExtras();
        if (extras == null || data == null) return;
        var callback = extras.getBinder("callback");
        if (callback == null || !callback.isBinderAlive()) return;
        var authority = data.getEncodedAuthority();
        if (authority == null) return;
        var s = authority.split(":", 2);
        if (s.length != 2) return;
        var packageName = s[0];
        int userId;
        try {
            userId = Integer.parseInt(s[1]);
        } catch (NumberFormatException e) {
            return;
        }
        var scopePackageName = data.getPath();
        if (scopePackageName == null) return;
        scopePackageName = scopePackageName.substring(1);
        var action = data.getQueryParameter("action");
        if (action == null) return;

        var iCallback = IXposedScopeCallback.Stub.asInterface(callback);
        try {
            var applicationInfo = PackageService.getApplicationInfo(scopePackageName, 0, userId);
            if (applicationInfo == null) {
                iCallback.onScopeRequestFailed(scopePackageName, "Package not found");
                return;
            }

            switch (action) {
                case "approve" -> {
                    ConfigManager.getInstance().setModuleScope(packageName, scopePackageName, userId);
                    iCallback.onScopeRequestApproved(scopePackageName);
                }
                case "deny" -> iCallback.onScopeRequestDenied(scopePackageName);
                case "delete" -> iCallback.onScopeRequestTimeout(scopePackageName);
                case "block" -> {
                    ConfigManager.getInstance().blockScopeRequest(packageName);
                    iCallback.onScopeRequestDenied(scopePackageName);
                }
            }
            Log.i(TAG, action + " scope " + scopePackageName + " for " + packageName + " in user " + userId);
        } catch (RemoteException e) {
            try {
                iCallback.onScopeRequestFailed(scopePackageName, e.getMessage());
            } catch (RemoteException ignored) {
                // callback died
            }
        }
        LSPNotificationManager.cancelNotification(SCOPE_CHANNEL_ID, packageName, userId);
    }

    private void registerReceiver(List<IntentFilter> filters, String requiredPermission, int userId, Consumer<Intent> task, int flag) {
        var receiver = new IIntentReceiver.Stub() {
            @Override
            public void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser) {
                getExecutorService().submit(() -> {
                    try {
                        task.accept(intent);
                    } catch (Throwable t) {
                        Log.e(TAG, "performReceive: ", t);
                    }
                });
                if (!ordered && !Objects.equals(intent.getAction(), Intent.ACTION_LOCKED_BOOT_COMPLETED))
                    return;
                try {
                    ActivityManagerService.finishReceiver(this, appThread, resultCode, data, extras, false, intent.getFlags());
                } catch (RemoteException e) {
                    Log.e(TAG, "finish receiver", e);
                }
            }
        };
        try {
            for (var filter : filters) {
                ActivityManagerService.registerReceiver("android", null, receiver, filter, requiredPermission, userId, flag);
            }
        } catch (RemoteException e) {
            Log.e(TAG, "register receiver", e);
        }
    }

    private void registerReceiver(List<IntentFilter> filters, int userId, Consumer<Intent> task) {
        //noinspection InlinedApi
        registerReceiver(filters, "android.permission.BRICK", userId, task, Context.RECEIVER_NOT_EXPORTED);
    }

    private void registerPackageReceiver() {
        var packageFilter = new IntentFilter();
        packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
        packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
        packageFilter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED);
        packageFilter.addDataScheme("package");

        var uidFilter = new IntentFilter(Intent.ACTION_UID_REMOVED);

        registerReceiver(List.of(packageFilter, uidFilter), -1, this::dispatchPackageChanged);
        Log.d(TAG, "registered package receiver");
    }

    private void registerConfigurationReceiver() {
        var intentFilter = new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED);

        registerReceiver(List.of(intentFilter), 0, this::dispatchConfigurationChanged);
        Log.d(TAG, "registered configuration receiver");
    }

    private void registerSecretCodeReceiver() {
        IntentFilter intentFilter = new IntentFilter();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            intentFilter.addAction(TelephonyManager.ACTION_SECRET_CODE);
        } else {
            // noinspection InlinedApi
            intentFilter.addAction(Telephony.Sms.Intents.SECRET_CODE_ACTION);
        }
        intentFilter.addDataAuthority("5776733", null);
        intentFilter.addDataScheme("android_secret_code");

        //noinspection InlinedApi
        registerReceiver(List.of(intentFilter), "android.permission.CONTROL_INCALL_EXPERIENCE",
                0, this::dispatchSecretCodeReceive, Context.RECEIVER_EXPORTED);
        Log.d(TAG, "registered secret code receiver");
    }

    private void registerBootCompleteReceiver() {
        var intentFilter = new IntentFilter(Intent.ACTION_LOCKED_BOOT_COMPLETED);
        intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
        registerReceiver(List.of(intentFilter), 0, this::dispatchBootCompleted);
        Log.d(TAG, "registered boot receiver");
    }

    private void registerUserChangeReceiver() {
        var userFilter = new IntentFilter();
        userFilter.addAction(ACTION_USER_ADDED);
        userFilter.addAction(ACTION_USER_REMOVED);

        registerReceiver(List.of(userFilter), -1, this::dispatchUserChanged);
        Log.d(TAG, "registered user info change receiver");
    }

    private void registerOpenManagerReceiver() {
        var intentFilter = new IntentFilter(LSPNotificationManager.openManagerAction);
        var moduleFilter = new IntentFilter(intentFilter);
        moduleFilter.addDataScheme("module");

        registerReceiver(List.of(intentFilter, moduleFilter), 0, this::dispatchOpenManager);
        Log.d(TAG, "registered open manager receiver");
    }

    private void registerModuleScopeReceiver() {
        var intentFilter = new IntentFilter(LSPNotificationManager.moduleScope);
        intentFilter.addDataScheme("module");

        registerReceiver(List.of(intentFilter), 0, this::dispatchModuleScope);
        Log.d(TAG, "registered module scope receiver");
    }

    private void registerUidObserver() {
        try {
            var which = HiddenApiBridge.ActivityManager_UID_OBSERVER_ACTIVE()
                    | HiddenApiBridge.ActivityManager_UID_OBSERVER_GONE()
                    | HiddenApiBridge.ActivityManager_UID_OBSERVER_IDLE()
                    | HiddenApiBridge.ActivityManager_UID_OBSERVER_CACHED();
            LSPModuleService.uidClear();
            ActivityManagerService.registerUidObserver(new IUidObserver.Stub() {
                @Override
                public void onUidActive(int uid) {
                    LSPModuleService.uidStarts(uid);
                }

                @Override
                public void onUidCachedChanged(int uid, boolean cached) {
                    if (!cached) LSPModuleService.uidStarts(uid);
                }

                @Override
                public void onUidIdle(int uid, boolean disabled) {
                    LSPModuleService.uidStarts(uid);
                }

                @Override
                public void onUidGone(int uid, boolean disabled) {
                    LSPModuleService.uidGone(uid);
                }
            }, which, HiddenApiBridge.ActivityManager_PROCESS_STATE_UNKNOWN(), null);
        } catch (RemoteException e) {
            Log.e(TAG, "registerUidObserver", e);
        }
    }

    @Override
    public void dispatchSystemServerContext(IBinder appThread, IBinder activityToken, String api) {
        Log.d(TAG, "received system context");
        this.appThread = appThread;
        ConfigManager.getInstance().setApi(api);
        ActivityManagerService.onSystemServerContext(IApplicationThread.Stub.asInterface(appThread), activityToken);
        registerBootCompleteReceiver();
        registerPackageReceiver();
        registerConfigurationReceiver();
        registerSecretCodeReceiver();
        registerUserChangeReceiver();
        registerOpenManagerReceiver();
        registerModuleScopeReceiver();
        registerUidObserver();
    }

    @Override
    public boolean preStartManager() {
        return ServiceManager.getManagerService().preStartManager();
    }

    @Override
    public boolean setManagerEnabled(boolean enabled) throws RemoteException {
        return ServiceManager.getManagerService().setEnabled(enabled);
    }
}
