package org.lsposed.lspd.service;

import android.annotation.SuppressLint;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.SELinux;
import android.os.SystemProperties;
import android.system.Os;
import android.util.Log;

import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.LinkedHashMap;

public class LogcatService implements Runnable {
    private static final String TAG = "LSPosedLogcat";
    private static final int mode = ParcelFileDescriptor.MODE_WRITE_ONLY |
            ParcelFileDescriptor.MODE_CREATE |
            ParcelFileDescriptor.MODE_TRUNCATE |
            ParcelFileDescriptor.MODE_APPEND;
    private int modulesFd = -1;
    private int verboseFd = -1;
    private Thread thread = null;

    static class LogLRU extends LinkedHashMap<File, Object> {
        private static final int MAX_ENTRIES = 10;

        public LogLRU() {
            super(MAX_ENTRIES, 1f, false);
        }

        @Override
        synchronized protected boolean removeEldestEntry(Entry<File, Object> eldest) {
            if (size() > MAX_ENTRIES && eldest.getKey().delete()) {
                Log.d(TAG, "Deleted old log " + eldest.getKey().getAbsolutePath());
                return true;
            }
            return false;
        }
    }

    @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
    private final LinkedHashMap<File, Object> moduleLogs = new LogLRU();
    @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
    private final LinkedHashMap<File, Object> verboseLogs = new LogLRU();

    @SuppressLint("UnsafeDynamicallyLoadedCode")
    public LogcatService() {
        String classPath = System.getProperty("java.class.path");
        var abi = Process.is64Bit() ? Build.SUPPORTED_64_BIT_ABIS[0] : Build.SUPPORTED_32_BIT_ABIS[0];
        System.load(classPath + "!/lib/" + abi + "/" + System.mapLibraryName("daemon"));
        ConfigFileManager.moveLogDir();

        // Meizu devices set this prop and prevent debug logs from being recorded
        if (SystemProperties.getInt("persist.sys.log_reject_level", 0) > 0) {
            SystemProperties.set("persist.sys.log_reject_level", "0");
        }

        getprop();
        dmesg();
    }

    private static void getprop() {
        // multithreaded process can not change their context type,
        // start a new process to set restricted context to filter privacy props
        var cmd = "echo -n u:r:untrusted_app:s0 > /proc/thread-self/attr/current; getprop";
        try {
            SELinux.setFSCreateContext("u:object_r:app_data_file:s0");
            new ProcessBuilder("sh", "-c", cmd)
                    .redirectOutput(ConfigFileManager.getPropsPath())
                    .start();
        } catch (IOException e) {
            Log.e(TAG, "getprop: ", e);
        } finally {
            SELinux.setFSCreateContext(null);
        }
    }

    private static void dmesg() {
        try {
            new ProcessBuilder("dmesg")
                    .redirectOutput(ConfigFileManager.getKmsgPath())
                    .start();
        } catch (IOException e) {
            Log.e(TAG, "dmesg: ", e);
        }
    }

    private native void runLogcat();

    @Override
    public void run() {
        Log.i(TAG, "start running");
        runLogcat();
        Log.i(TAG, "stopped");
    }

    @SuppressWarnings("unused")
    private int refreshFd(boolean isVerboseLog) {
        try {
            File log;
            if (isVerboseLog) {
                checkFd(verboseFd);
                log = ConfigFileManager.getNewVerboseLogPath();
            } else {
                checkFd(modulesFd);
                log = ConfigFileManager.getNewModulesLogPath();
            }
            Log.i(TAG, "New log file: " + log);
            ConfigFileManager.chattr0(log.toPath().getParent());
            int fd = ParcelFileDescriptor.open(log, mode).detachFd();
            if (isVerboseLog) {
                synchronized (verboseLogs) {
                    verboseLogs.put(log, new Object());
                }
                verboseFd = fd;
            } else {
                synchronized (moduleLogs) {
                    moduleLogs.put(log, new Object());
                }
                modulesFd = fd;
            }
            return fd;
        } catch (IOException e) {
            if (isVerboseLog) verboseFd = -1;
            else modulesFd = -1;
            Log.w(TAG, "refreshFd", e);
            return -1;
        }
    }

    private static void checkFd(int fd) {
        if (fd == -1) return;
        try {
            var jfd = new FileDescriptor();
            //noinspection JavaReflectionMemberAccess DiscouragedPrivateApi
            jfd.getClass().getDeclaredMethod("setInt$", int.class).invoke(jfd, fd);
            var stat = Os.fstat(jfd);
            if (stat.st_nlink == 0) {
                var file = Files.readSymbolicLink(fdToPath(fd));
                var parent = file.getParent();
                if (!Files.isDirectory(parent, LinkOption.NOFOLLOW_LINKS)) {
                    if (ConfigFileManager.chattr0(parent))
                        Files.deleteIfExists(parent);
                }
                var name = file.getFileName().toString();
                var originName = name.substring(0, name.lastIndexOf(' '));
                Files.copy(file, parent.resolve(originName));
            }
        } catch (Throwable e) {
            Log.w(TAG, "checkFd " + fd, e);
        }
    }

    public boolean isRunning() {
        return thread != null && thread.isAlive();
    }

    public void start() {
        if (isRunning()) return;
        thread = new Thread(this);
        thread.setName("logcat");
        thread.setUncaughtExceptionHandler((t, e) -> {
            Log.e(TAG, "Crash unexpectedly: ", e);
            thread = null;
            start();
        });
        thread.start();
    }

    public void startVerbose() {
        Log.i(TAG, "!!start_verbose!!");
    }

    public void stopVerbose() {
        Log.i(TAG, "!!stop_verbose!!");
    }

    public void enableWatchdog() {
        Log.i(TAG, "!!start_watchdog!!");
    }

    public void disableWatchdog() {
        Log.i(TAG, "!!stop_watchdog!!");
    }

    public void refresh(boolean isVerboseLog) {
        if (isVerboseLog) {
            Log.i(TAG, "!!refresh_verbose!!");
        } else {
            Log.i(TAG, "!!refresh_modules!!");
        }
    }

    private static Path fdToPath(int fd) {
        if (fd == -1) return null;
        else return Paths.get("/proc/self/fd", String.valueOf(fd));
    }

    public File getVerboseLog() {
        var path = fdToPath(verboseFd);
        return path == null ? null : path.toFile();
    }

    public File getModulesLog() {
        var path = fdToPath(modulesFd);
        return path == null ? null : path.toFile();
    }

    public void checkLogFile() {
        if (modulesFd == -1)
            refresh(false);
        if (verboseFd == -1)
            refresh(true);
    }
}
