package io.xpipe.app.process;

import io.xpipe.app.util.GroupFile;
import io.xpipe.app.util.PasswdFile;
import io.xpipe.app.util.FailableSupplier;
import io.xpipe.app.util.FilePath;
import io.xpipe.app.util.OsType;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;

public class ShellView {

    protected final ShellControl shellControl;
    protected String user;
    protected FilePath userHome;
    protected Boolean root;
    protected Boolean administrator;
    protected PasswdFile passwdFile;
    protected GroupFile groupFile;
    protected String recognized;
    protected final Map<String, Boolean> installedApplications = new HashMap<>();
    protected final Map<String, Boolean> genericCache = new HashMap<>();

    public ShellView(ShellControl shellControl) {
        this.shellControl = shellControl;
    }

    public void checkSupported() {
        shellControl.getShellDialect().getDumbMode().throwIfUnsupported();
    }

    protected ShellDialect getDialect() {
        return shellControl.getShellDialect();
    }

    public synchronized void setCachedPredicate(String key, boolean value) {
        genericCache.put(key, value);
    }

    public synchronized boolean getCachedPredicate(String key, FailableSupplier<Boolean> supplier) throws Exception {
        var v = genericCache.get(key);
        if (v != null) {
            return v;
        }

        var supplied = supplier.get();
        genericCache.put(key, supplied);
        return supplied;
    }

    public synchronized PasswdFile getPasswdFile() throws Exception {
        if (passwdFile == null) {
            passwdFile = PasswdFile.parse(shellControl);
        }

        return passwdFile;
    }

    public synchronized GroupFile getGroupFile() throws Exception {
        if (groupFile == null) {
            groupFile = GroupFile.parse(shellControl);
        }

        return groupFile;
    }

    public FilePath writeTextFileDeterministic(FilePath base, String text) throws Exception {
        var hash = Math.abs(Objects.hash(text, user()));
        var ext = base.getExtension();
        var target = FilePath.of(base.getBaseName().toString() + "-" + hash + (ext.isPresent() ? "." + ext.get() : ""));
        if (fileExists(target)) {
            return target;
        }
        writeTextFile(target, text);
        return target;
    }

    public byte[] readRawFile(FilePath path) throws Exception {
        var s = getDialect().getFileReadCommand(shellControl, path.toString()).readRawBytesOrThrow();
        return s;
    }

    public String readTextFile(FilePath path) throws Exception {
        var s = getDialect().getFileReadCommand(shellControl, path.toString()).readStdoutOrThrow();
        return s;
    }

    public void writeRawFile(FilePath path, byte[] data) throws Exception {
        writeStreamFile(path, new ByteArrayInputStream(data), data.length);
    }

    public void writeTextFile(FilePath path, String text) throws Exception {
        var cc = getDialect().createTextFileWriteCommand(shellControl, text, path.toString());
        cc.execute();
    }

    public void writeScriptFile(FilePath path, String text) throws Exception {
        var cc = getDialect().createScriptTextFileWriteCommand(shellControl, text, path.toString());
        cc.execute();
    }

    public FilePath writeScriptFileDeterministic(FilePath base, String text) throws Exception {
        var hash = Math.abs(Objects.hash(text, user()));
        var ext = base.getExtension();
        var target = FilePath.of(base.getBaseName().toString() + "-" + hash + (ext.isPresent() ? "." + ext.get() : ""));
        if (fileExists(target)) {
            return target;
        }
        writeScriptFile(target, text);
        return target;
    }

    public void writeStreamFile(FilePath path, InputStream inputStream, long size) throws Exception {
        try (var out = getDialect()
                .createStreamFileWriteCommand(shellControl, path.toString(), size)
                .startExternalStdin()) {
            inputStream.transferTo(out);
        }
    }

    public FilePath userHome() throws Exception {
        if (userHome == null) {
            userHome = FilePath.of(OsFileSystem.of(shellControl.getOsType()).getUserHomeDirectory(shellControl));
        }

        return userHome;
    }

    public void moveFile(FilePath source, FilePath dest) throws Exception {
        getDialect()
                .getFileMoveCommand(shellControl, source.toString(), dest.toString())
                .execute();
    }

    public boolean fileExists(FilePath path) throws Exception {
        return getDialect()
                .createFileExistsCommand(shellControl, path.toString())
                .executeAndCheck();
    }

    public String getRecognizedArch() throws Exception {
        if (shellControl.getOsType() == OsType.WINDOWS) {
            var env = getEnvironmentVariable("PROCESSOR_ARCHITECTURE").orElse(null);
            var effective = "ARM64".equals(env) ? "arm64" : "x86_64";
            return recognized = effective;
        } else {
            var uname =
                    shellControl.command(CommandBuilder.of().add("uname", "-m")).readStdoutIfPossible();
            return recognized = "arm64".equals(uname.orElse("x86_64")) ? "arm64" : "x86_64";
        }
    }

    public void deleteDirectory(FilePath path) throws Exception {
        getDialect().deleteFileOrDirectory(shellControl, path.toString()).execute();
    }

    public void deleteFile(FilePath path) throws Exception {
        getDialect().getFileDeleteCommand(shellControl, path.toString()).execute();
    }

    public void deleteFileIfPossible(FilePath path) throws Exception {
        getDialect().getFileDeleteCommand(shellControl, path.toString()).executeAndCheck();
    }

    public void mkdir(FilePath path) throws Exception {
        shellControl.command(getDialect().getMkdirsCommand(path.toString())).execute();
    }

    public boolean directoryExists(FilePath path) throws Exception {
        return getDialect().directoryExists(shellControl, path.toString()).executeAndCheck();
    }

    public String user() throws Exception {
        if (user == null) {
            user = getDialect().printUsernameCommand(shellControl).readStdoutOrThrow();
        }

        return user;
    }

    public boolean isRoot() throws Exception {
        if (shellControl.getOsType() == OsType.WINDOWS) {
            return false;
        }

        if (root != null) {
            return root;
        }

        var isRoot = shellControl.executeSimpleBooleanCommand("test \"${EUID:-$(id -u)}\" -eq 0");
        return (root = isRoot);
    }

    public Optional<FilePath> findProgram(String name) throws Exception {
        var out = shellControl
                .command(shellControl.getShellDialect().whichCommand(shellControl, name))
                .readStdoutIfPossible();
        return out.flatMap(s -> s.lines().findFirst()).map(String::trim).map(s -> FilePath.of(s));
    }

    public void transferLocalFile(Path localPath, FilePath target) throws Exception {
        try (var in = Files.newInputStream(localPath)) {
            writeStreamFile(target, in, in.available());
        }
    }

    public synchronized boolean isInPath(String executable, boolean cache) throws Exception {
        if (cache) {
            var r = installedApplications.get(executable);
            if (r != null) {
                return r;
            }

            var found = findProgram(executable).isPresent();
            installedApplications.put(executable, found);
            return found;
        }

        return findProgram(executable).isPresent();
    }

    public void cd(FilePath directory) throws Exception {
        cd(directory.toString());
    }

    public FilePath pwd() throws Exception {
        return FilePath.of(shellControl
                .command(shellControl.getShellDialect().getPrintWorkingDirectoryCommand())
                .readStdoutOrThrow());
    }

    public void touch(FilePath path) throws Exception {
        var c = shellControl.getShellDialect().getFileTouchCommand(shellControl, path.toString());
        c.execute();
    }

    public void cd(String directory) throws Exception {
        var d = shellControl.getShellDialect();
        var cmd = shellControl.command(d.getCdCommand(directory));
        cmd.executeAndCheck();
    }

    public void unsetEnvironmentVariable(String name) throws Exception {
        shellControl
                .command(shellControl.getShellDialect().unsetEnvironmentVariableCommand(name))
                .executeAndCheck();
    }

    public Optional<String> getEnvironmentVariable(String name) throws Exception {
        var r = shellControl
                .command(shellControl.getShellDialect().getPrintEnvironmentVariableCommand(name))
                .readStdoutOrThrow();
        if (r.isBlank() || r.equals(getDialect().environmentVariable(name))) {
            return Optional.empty();
        }

        return Optional.of(r);
    }

    public String getEnvironmentVariableOrThrow(String name) throws Exception {
        var r = getEnvironmentVariable(name);
        return r.orElseThrow(
                () -> new IllegalArgumentException("Required environment variable " + name + " not defined"));
    }

    public void setEnvironmentVariable(String name, String value) throws Exception {
        shellControl
                .command(shellControl.getShellDialect().getSetEnvironmentVariableCommand(name, value))
                .execute();
    }

    public void setSensitiveEnvironmentVariable(String name, String value) throws Exception {
        var command =
                shellControl.command(shellControl.getShellDialect().getSetEnvironmentVariableCommand(name, value));
        command.sensitive();
        command.execute();
    }

    public synchronized boolean isAdministrator() throws Exception {
        if (shellControl.getOsType() != OsType.WINDOWS) {
            return false;
        }

        if (administrator != null) {
            return administrator;
        }

        if (shellControl.getShellDialect() == ShellDialects.CMD) {
            administrator = shellControl.command("net.exe session 1>NUL 2>NUL").executeAndCheck();
        } else if (ShellDialects.isPowershell(shellControl)) {
            administrator = shellControl
                    .command(String.format(
                            "try {$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent());"
                                    + "if (-not $($currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))) {$host.ui"
                                    + ".WriteErrorLine(\"%s\"); throw \"error\"}} catch {}",
                            "Not Administrator"))
                    .executeAndCheck();
        } else {
            administrator = false;
        }

        return administrator;
    }
}
