package io.xpipe.app.cred;

import io.xpipe.app.issue.ErrorAction;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.process.*;
import io.xpipe.app.util.DocumentationLink;
import io.xpipe.app.util.FilePath;
import io.xpipe.app.util.OsType;

import com.sun.jna.Memory;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinBase;

import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicBoolean;

public class SshIdentityStateManager {

    private static RunningAgent runningAgent;

    private static boolean checkNamedPipeExists(Path path) {
        Memory p = new Memory(WinBase.WIN32_FIND_DATA.sizeOf());
        // This will not break the named pipe compared to using a normal exists check
        var r = Kernel32.INSTANCE.FindFirstFile(path.toString(), p);
        if (!WinBase.INVALID_HANDLE_VALUE.equals(r)) {
            Kernel32.INSTANCE.FindClose(r);
            return true;
        } else {
            return false;
        }
    }

    private static void stopWindowsAgents(boolean openssh, boolean gpg, boolean external) throws Exception {
        var pipePath = Path.of("\\\\.\\pipe\\openssh-ssh-agent");
        if (!checkNamedPipeExists(pipePath)) {
            return;
        }

        try (var sc = LocalShell.getShell().start()) {
            var gpgList = sc.executeSimpleStringCommand("TASKLIST /FI \"IMAGENAME eq gpg-agent.exe\"");
            var gpgRunning = gpgList.contains("gpg-agent.exe");

            var opensshList = sc.executeSimpleStringCommand("TASKLIST /FI \"IMAGENAME eq ssh-agent.exe\"");
            var opensshRunning = opensshList.contains("ssh-agent.exe");

            if (external && !gpgRunning && !opensshRunning) {
                throw ErrorEventFactory.expected(new IllegalStateException(
                        "An external password manager agent is running, but XPipe requested to use another SSH agent. You have to disable the other running "
                                + "password manager agent first."));
            }

            if (gpg && gpgRunning) {
                // This sometimes takes a long time if the agent is not running. Why?
                sc.command(CommandBuilder.of().add("gpg-connect-agent", "killagent", "/bye"))
                        .executeAndCheck();
            }

            if (openssh && opensshRunning) {
                var msg =
                        "The Windows OpenSSH agent is running. This will cause it to interfere with other agents. You have to manually stop the "
                                + "running ssh-agent service to allow other agents to work";
                var r = new AtomicBoolean();
                var event = ErrorEventFactory.fromMessage(msg).expected();
                var shutdown = new ErrorAction() {
                    @Override
                    public String getName() {
                        return "Shut down ssh-agent service";
                    }

                    @Override
                    public String getDescription() {
                        return "Stop the agent service as an administrator";
                    }

                    @Override
                    public boolean handle(ErrorEvent event) {
                        r.set(true);
                        return true;
                    }
                };
                event.customAction(shutdown).handle();

                if (r.get()) {
                    sc.command(
                                    "powershell -Command \"Start-Process cmd -Wait -ArgumentList /c, sc, stop, ssh-agent -Verb runAs\"")
                            .executeAndCheck();
                }
            }
        }
    }

    private static void checkLocalAgentIdentities(String socketEvn) throws Exception {
        try (var sc = LocalShell.getShell().start()) {
            checkAgentIdentities(sc, socketEvn);
        }
    }

    public static synchronized void checkAgentIdentities(ShellControl sc, String authSock) throws Exception {
        var found = sc.view().findProgram("ssh-add");
        if (found.isEmpty()) {
            throw ErrorEventFactory.expected(new IllegalStateException(
                    "SSH agent tool ssh-add not found in PATH. Is the SSH agent correctly installed?"));
        }

        try (var c = sc.command(CommandBuilder.of().add("ssh-add", "-l").fixedEnvironment("SSH_AUTH_SOCK", authSock))
                .start()) {
            var r = c.readStdoutAndStderr();
            if (c.getExitCode() != 0) {
                var posixMessage = sc.getOsType() != OsType.WINDOWS
                        ? authSock != null ? " at the socket location " + authSock : ""
                        : "";
                var joined = (!r[0].isEmpty() ? r[0] + "\n" : "") + r[1];
                var ex = new IllegalStateException("Unable to list agent identities via command ssh-add -l:\n" + joined
                        + "\n\n" + "Please check whether the agent is running correctly%s.".formatted(posixMessage));
                var eventBuilder = ErrorEventFactory.fromThrowable(ex).expected();
                if (joined.toLowerCase().contains("signing failed: agent refused operation")
                        || joined.toLowerCase().contains("error connecting to agent: connecting refused")) {
                    eventBuilder.documentationLink(DocumentationLink.SSH_AGENT_REFUSAL);
                }
                ErrorEventFactory.preconfigure(eventBuilder);
                throw ex;
            }
        } catch (ProcessOutputException ex) {
            if (sc.getOsType() == OsType.WINDOWS && ex.getOutput().contains("No such file or directory")) {
                throw ProcessOutputException.withPrefix(
                        "Failed to connect to the OpenSSH agent service. Is the Windows OpenSSH feature enabled and the OpenSSH Authentication "
                                + "Agent service running?",
                        ex);
            } else {
                throw ex;
            }
        }
    }

    public static synchronized void prepareLocalExternalAgent(FilePath socket) throws Exception {
        if (runningAgent == RunningAgent.EXTERNAL_AGENT) {
            return;
        }

        if (OsType.ofLocal() == OsType.WINDOWS) {
            stopWindowsAgents(true, true, false);

            var pipePath = Path.of("\\\\.\\pipe\\openssh-ssh-agent");
            var pipeExists = checkNamedPipeExists(pipePath);
            if (!pipeExists) {
                // No agent is running
                throw ErrorEventFactory.expected(new IllegalStateException(
                        "An external agent is configured, but no external SSH agent is running. Make sure that the external "
                                + "agent is started"));
            }
        }

        checkLocalAgentIdentities(socket != null ? socket.toString() : null);

        runningAgent = RunningAgent.EXTERNAL_AGENT;
    }

    public static synchronized void prepareLocalGpgAgent() throws Exception {
        if (runningAgent == RunningAgent.GPG_AGENT) {
            return;
        }

        try (var sc = LocalShell.getShell().start()) {
            CommandSupport.isInPathOrThrow(sc, "gpg-connect-agent", "GPG connect agent executable", null);

            FilePath dir;
            if (sc.getOsType() == OsType.WINDOWS) {
                stopWindowsAgents(true, false, true);
                var appdata = FilePath.of(sc.view().getEnvironmentVariableOrThrow("APPDATA"))
                        .join("gnupg");
                dir = appdata;
            } else {
                dir = sc.view().userHome().join(".gnupg");
            }

            sc.view().mkdir(dir);
            var confFile = dir.join("gpg-agent.conf");
            var content = sc.view().fileExists(confFile) ? sc.view().readTextFile(confFile) : "";

            if (sc.getOsType() == OsType.WINDOWS) {
                if (!content.contains("enable-win32-openssh-support")) {
                    content += "\nenable-win32-openssh-support\n";
                    sc.view().writeTextFile(confFile, content);
                }
                // reloadagent does not work correctly, so kill it
                stopWindowsAgents(true, true, false);
                sc.command(CommandBuilder.of().add("gpg-connect-agent", "/bye")).execute();
                checkLocalAgentIdentities(null);
            } else {
                if (!content.contains("enable-ssh-support")) {
                    content += "\nenable-ssh-support\n";
                    sc.view().writeTextFile(confFile, content);
                    sc.executeSimpleCommand(CommandBuilder.of().add("gpg-connect-agent", "reloadagent", "/bye"));
                } else {
                    sc.executeSimpleCommand(CommandBuilder.of().add("gpg-connect-agent", "/bye"));
                }
                var socketEnv =
                        sc.command("gpgconf --list-dirs agent-ssh-socket").readStdoutOrThrow();
                checkLocalAgentIdentities(socketEnv);
            }
        }

        runningAgent = RunningAgent.GPG_AGENT;
    }

    public static synchronized void prepareLocalOpenSshAgent(ShellControl sc, FilePath socket) throws Exception {
        if (runningAgent == RunningAgent.SSH_AGENT) {
            return;
        }

        if (sc.getOsType() == OsType.WINDOWS) {
            CommandSupport.isInPathOrThrow(sc, "ssh-agent", "SSH Agent", null);
            stopWindowsAgents(false, true, true);
            sc.executeSimpleBooleanCommand("ssh-agent start");
            checkLocalAgentIdentities(null);
        } else {
            checkLocalAgentIdentities(
                    socket != null
                            ? socket.resolveTildeHome(sc.view().userHome()).toString()
                            : null);
        }

        runningAgent = RunningAgent.SSH_AGENT;
    }

    public static synchronized void prepareLocalCustomAgent(ShellControl sc, FilePath socket) throws Exception {
        if (runningAgent == RunningAgent.CUSTOM_AGENT) {
            return;
        }

        if (sc.getOsType() == OsType.WINDOWS) {
            checkLocalAgentIdentities(null);
        } else {
            checkLocalAgentIdentities(
                    socket != null
                            ? socket.resolveTildeHome(sc.view().userHome()).toString()
                            : null);
        }

        runningAgent = RunningAgent.CUSTOM_AGENT;
    }

    private enum RunningAgent {
        SSH_AGENT,
        CUSTOM_AGENT,
        GPG_AGENT,
        EXTERNAL_AGENT
    }
}
