package files;
import Common.Constants;
import Common.Utils.Utils;
import Common.Utils.Validators.ShellParser;
import GlobalData.Machine.Machine;
import GlobalData.RemoteFile.RemoteFile;
import GlobalData.User.User;
import ProjectData.Project.db_project_info;
import Visual_DVM_2021.Passes.PassException;
import Visual_DVM_2021.Passes.Pass_2021;
import Visual_DVM_2021.Passes.SSH.WorkspaceNotFoundException;
import com.jcraft.jsch.*;
import javafx.util.Pair;

import java.io.*;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Vector;
//убираем лишний класс-прослойку.
//старый server pass Пока не трогаем.
//https://stackoverflow.com/questions/15108923/sftp-file-transfer-using-java-jsch
//https://javadevblog.com/kak-dobavit-biblioteku-jar-fajl-v-proekt-intellij-idea.html
//http://www.jcraft.com/jsch/
//http://developer-remarks.blogspot.com/2013/05/ssh-via-jsch-example.html
//https://stackoverflow.com/questions/4194439/sending-commands-to-server-via-jsch-shell-channel
//todo вывести его. уже есть UserConnection.
public abstract class ConnectionPass<T> extends Pass_2021<T> {
    //-----------------------------------------------
    public static final String modules = "modules";
    //--------------
    public static final String starter = "starter";
    public static final String launcher = "launcher";
    public static final String planner = "planner";
    //--------------
    public static final String Process_r_header = "Process_r.h";
    public static final String starter_code = "starter.cpp";
    public static final String launcher_code = "launcher.cpp";
    //--------------
    public static final String projects = "projects";
    public static final String compilers = "compilers";
    public static final String tests = "tests";
    public Machine machine = null;
    public User user = null;
    //тут как в WinScp  - 2 независимых канала. один для файлов. другой для команд.
    public ChannelSftp sftpChannel = null;
    public ChannelShell shellChannel = null;
    public ChannelExec execChannel = null;
    //----------------------------------------------------
    protected JSch jsch;
    protected Session session = null;
    protected boolean needsInitialize() {
        return true;
    }
    //-------------------------------------
    protected boolean isConnected = false; //пока
    public ChannelSftp getSftpChannel() {
        return sftpChannel;
    }
    //учитывать. что пока сокет не создан, прервать соединение нельзя.
    public void Connect() throws Exception {
        isConnected = false;
        session = (jsch = new JSch()).getSession(user.login, machine.address, machine.port);
        switch (user.authentication) {
            case password:
                session.setPassword(user.password);
                break;
                /* пока не совсем ясно как это делать.
            case key:
               // https://stackoverflow.com/questions/47422025/java-sftp-authentication-with-ppk-file
                //todo перешифровать ключ https://www.example-code.com/java/ssh_ppk_to_pem.asp
                jsch.addIdentity(user.password);
                break;

                 */
        }
        session.setConfig("StrictHostKeyChecking", "no");
        //UI.Print("Establishing Connection...");
        ShowMessage1("соединение с машиной " + Utils.Brackets(machine.getURL()) + "..");
        session.connect(0);
        //UI.Print("Connection established.");
        //UI.Print("Creating SFTP Channel.");
        sftpChannel = (ChannelSftp) session.openChannel("sftp");
        sftpChannel.connect();
        isConnected = true; // теперь можно прерывать метод.
        if (needsInitialize()) {
            RemoteFile userWorkspace = new RemoteFile(user.workspace, true);
            if (!Exists(userWorkspace))
                throw new WorkspaceNotFoundException(
                        "Рабочее пространство пользователя " + Utils.Brackets(user.login)
                                + " на машине " + Utils.Brackets(machine.getURL())
                                + "\n" + Utils.Brackets(user.workspace) + "\nне найдено.\n" +
                                "Требуется выполнить повторную инициализацию пользователя."
                );
        }
    }
    public void Disconnect() {
        if (sftpChannel != null) sftpChannel.disconnect();
        if (execChannel != null) execChannel.disconnect();
        if (session != null) session.disconnect();
        //----------------------
        sftpChannel = null;
        execChannel = null;
        session = null;
        isConnected = false;
        //---------------------
//        UI.Print(DebugPrintLevel.Session, "session ended");
    }
    @Override
    protected void performFinish() throws Exception {
        Disconnect();
    }
    public void Command(String... commands) throws Exception {
        if (commands.length > 0) {
            String command = String.join("\n", commands);
            // UI.Print(DebugPrintLevel.Session, command);
            //  UI.Print(DebugPrintLevel.Session, "Creating Exec Channel.");
            execChannel = (ChannelExec) session.openChannel("exec");
            execChannel.setErrStream(System.err);
            execChannel.setCommand(command);
            execChannel.connect();
            BufferedReader in = new BufferedReader(new InputStreamReader(execChannel.getInputStream()));
            while (in.readLine() != null) ;
            execChannel.disconnect();
        }
    }
    public String CommandWithAnswer(char end, String... commands) throws Exception {
        String output = "";
        if (commands.length > 0) {
            String command = String.join("\n", commands);
            //  UI.Print(DebugPrintLevel.Session, command);
            //  UI.Print(DebugPrintLevel.Session, "Creating Exec Channel.");
            //    System.out.println(Utils.Brackets(command));
            execChannel = (ChannelExec) session.openChannel("exec");
            execChannel.setErrStream(System.err);
            InputStreamReader reader = new InputStreamReader(execChannel.getInputStream());
            execChannel.setCommand(command);
            execChannel.connect();
            char[] chars = new char[1];
            while (reader.read(chars) >= 0) if (chars[0] == end) break;
            else output += chars[0];
            execChannel.disconnect();
        }
        //  System.out.println(Utils.Brackets(output));
        return output;
    }
    //https://stackoverflow.com/questions/4194439/sending-commands-to-server-via-jsch-shell-channel
    @Override
    protected void CheckException(Exception ex) {
        if (ex instanceof WorkspaceNotFoundException) {
            Log.Writeln_(ex.getMessage());
            return;
        }
        Throwable cause = getCauseRec(ex);
        if ((cause instanceof UnknownHostException) || (cause instanceof SocketException)) {
            Log.Writeln(machine.getFullDescription() + " не найден(а).\n" + "Проверьте наличие подключения к сети.");
        } else if (cause instanceof SocketTimeoutException) {
            Log.Writeln(machine.getFullDescription() + " не отвечает.");
        } else super.CheckException(ex);
    }
    public Vector<String> read_file_lines(String path_r) throws Exception {
        Vector<String> res = new Vector<>();
        InputStream out = sftpChannel.get(path_r);
        BufferedReader br = new BufferedReader(new InputStreamReader(out));
        String line;
        while ((line = br.readLine()) != null) res.add(line);
        br.close();
        return res;
    }
    public void getSingleFile(String src, String dst) throws Exception {
        sftpChannel.get(src, dst);
    }
    //с проверкой.
    public boolean tryGetSingleFileWithMaxSize(RemoteFile src, File dst, int maxSize) throws Exception {
        if (Exists(src)) {
            if ((maxSize == 0) || (getFileKBSize(src.full_name) <= maxSize)) {
                getSingleFile(src.full_name, dst.getAbsolutePath());
                return true;
            } else {
                Utils.WriteToFile(dst, "Размер файла превышает " + maxSize + " KB.\n" + "Файл не загружен. Его можно просмотреть на машине по адресу\n" + Utils.Brackets(src.full_name));
            }
        }
        return false;
    }
    public void putSingleFile(File src, RemoteFile dst) throws Exception {
        sftpChannel.put(src.getAbsolutePath(), dst.full_name);
    }
    public void tryMKDir(RemoteFile dir) throws Exception {
        if (!Exists(dir)) sftpChannel.mkdir(dir.full_name);
    }
    public void tryRM(RemoteFile file) throws Exception {
        if (Exists(file))
            sftpChannel.rm(file.full_name);
    }
    public void putSingleFile(String src, String dst) throws Exception {
        sftpChannel.put(src, dst);
    }
    public void put_resource(String res_name) throws Exception {
        putSingleFile(Utils.CreateTempResourceFile(res_name).getAbsolutePath(), res_name);
    }
    @Override
    public void body() throws Exception {
        Connect();
        ServerAction();
    }
    protected void ServerAction() throws Exception {
    }
    //https://losst.ru/komanda-find-v-linux#%D0%9E%D1%81%D0%BD%D0%BE%D0%B2%D0%BD%D1%8B%D0%B5_%D0%BF%D0%B0%D1%80%D0%B0%D0%BC%D0%B5%D1%82%D1%80%D1%8B_%D0%BA%D0%BE%D0%BC%D0%B0%D0%BD%D0%B4%D1%8B_find
    public String getStarter() {
        return String.join("/", user.workspace, modules, starter);
    }
    public String getLauncher() {
        return String.join("/", user.workspace, modules, launcher);
    }
    //насчет даты.
    //http://i-leon.ru/tools/time
    //этот метод для того чтобы не пересылать подверсии. В случае тестов их наличие исключено!
    public void SynchronizeProjectSubDirsR(db_project_info project, File local_dir, RemoteFile remote_dir, boolean data) throws Exception {
        ShowMessage2("синхронизация: " + local_dir.getName());
        Vector<File> local_subdirs = project.getSubdirectoriesSimple(local_dir);
        Vector<File> local_files = project.getActiveFilesForSynchronization(local_dir, data);
        //------------------------------------------------------------------------
        LinkedHashMap<String, RemoteFile> remote_subdirs = new LinkedHashMap<>();
        LinkedHashMap<String, RemoteFile> remote_files = new LinkedHashMap<>();
        Vector<ChannelSftp.LsEntry> files = sftpChannel.ls(remote_dir.full_name);
        for (ChannelSftp.LsEntry file : files) {
            if (file.getAttrs().isDir()) {
                if (!file.getFilename().equals(".") && !file.getFilename().equals(".."))
                    remote_subdirs.put(file.getFilename(), new RemoteFile(remote_dir.full_name, file.getFilename(), true));
            } else {
                RemoteFile rf = new RemoteFile(remote_dir.full_name, file.getFilename());
                rf.updateTime = RemoteFile.convertUpdateTime(file.getAttrs().getMTime());
                remote_files.put(file.getFilename(), rf);
            }
        }
        for (File lsd : local_subdirs) {
            RemoteFile rsd = null;
            if (!remote_subdirs.containsKey(lsd.getName()))
                sftpChannel.mkdir((rsd = new RemoteFile(remote_dir.full_name, lsd.getName(), true)).full_name);
            else rsd = remote_subdirs.get(lsd.getName());
            SynchronizeProjectSubDirsR(project, lsd, rsd, data);
        }
        for (File lf : local_files) {
            RemoteFile rf = null;
            if (!remote_files.containsKey(lf.getName())) {
                rf = new RemoteFile(remote_dir.full_name, lf.getName());
                ShowMessage2(lf.getName());
                putSingleFile(lf, rf);
            } else {
                rf = remote_files.get(lf.getName());
                if (lf.lastModified() > rf.updateTime) {
                    ShowMessage2(lf.getName());
                    putSingleFile(lf, rf);
                }
            }
        }
    }
    protected int getFileKBSize(String path) throws Exception {
        return Integer.parseInt(CommandWithAnswer('\t', "du " + Utils.DQuotes(path)));
    }
    //https://stackoverflow.com/questions/4194439/sending-commands-to-server-via-jsch-shell-channel
    public String ShellCommand(String... commands) throws Exception {
        shellChannel = (ChannelShell) session.openChannel("shell");
        PipedInputStream in = new PipedInputStream();
        PipedOutputStream out = new PipedOutputStream();
        shellChannel.setInputStream(in);
        shellChannel.setOutputStream(out);
        PipedOutputStream pin = new PipedOutputStream(in);
        PipedInputStream pout = new PipedInputStream(out);
        shellChannel.connect();
        InputStreamReader fromServer = new InputStreamReader(pout);
        ShellParser.setUserName(user.login);
        ShellParser.ReadInvitation(fromServer);
        //  System.out.println("first invitation read");
        String result = "";
        //формат работы с шеллом.
        //<эхо-команды\n><ответ команды><приглашение\n><приглашение>
        for (String command : commands) {
            // System.out.println(command);
            pin.write((command + "\r\n").getBytes());
            ShellParser.ReadLine(fromServer);
            //  UI.Info("echo read");
            ShellParser.ReadInvitation(fromServer);
            //  UI.Info("first invitation read");
            result = ShellParser.getCommandResult(fromServer);
            //  UI.Info("result = " + Utils.Brackets(result));
        }
        shellChannel.disconnect();
        return result;
    }
    public Vector<RemoteFile> getFilesByExtensions(RemoteFile dir, String... extensions) throws Exception {
        Vector<RemoteFile> res = new Vector<>();
        Vector<ChannelSftp.LsEntry> files = sftpChannel.ls(dir.full_name);
        for (ChannelSftp.LsEntry file : files) {
            String[] data = file.getFilename().split("\\.");
            if (data.length > 1) {
                String file_extension = data[data.length - 1];
                for (String extension : extensions) {
                    if (file_extension.equalsIgnoreCase(extension))
                        res.add(new RemoteFile(dir.full_name, file.getFilename()));
                }
            }
        }
        return res;
    }
    public void deleteFilesByExtensions(RemoteFile dir, String... extensions) throws Exception {
        Vector<RemoteFile> to_delete = getFilesByExtensions(dir, extensions);
        for (RemoteFile file : to_delete)
            sftpChannel.rm(file.full_name);
    }
    public void copy(RemoteFile src, RemoteFile dst) throws Exception {
        ShellCommand("cp " + Utils.DQuotes(src.full_name) + " " + Utils.DQuotes(dst.full_name));
    }
    //-------
    public void writeToFile(String text, RemoteFile dst) throws Exception {
        sftpChannel.put(new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8)), dst.full_name);
        sftpChannel.chmod(0777, dst.full_name);
    }
    public void SynchronizeSubDirsR(File local_dir, RemoteFile remote_dir) throws Exception {
        File[] local_subdirs = local_dir.listFiles(File::isDirectory);
        File[] local_files = local_dir.listFiles(File::isFile);
        //------------------------------------------------------------------------
        LinkedHashMap<String, RemoteFile> remote_subdirs = new LinkedHashMap<>();
        LinkedHashMap<String, RemoteFile> remote_files = new LinkedHashMap<>();
        Vector<ChannelSftp.LsEntry> files = sftpChannel.ls(remote_dir.full_name);
        for (ChannelSftp.LsEntry file : files) {
            if (file.getAttrs().isDir()) {
                if (!file.getFilename().equals(".") && !file.getFilename().equals(".."))
                    remote_subdirs.put(file.getFilename(), new RemoteFile(remote_dir.full_name, file.getFilename(), true));
            } else {
                RemoteFile rf = new RemoteFile(remote_dir.full_name, file.getFilename());
                rf.updateTime = RemoteFile.convertUpdateTime(file.getAttrs().getMTime());
                remote_files.put(file.getFilename(), rf);
            }
        }
        if (local_subdirs != null) {
            for (File lsd : local_subdirs) {
                if (!lsd.getName().equals(Constants.data)) {
                    RemoteFile rsd = null;
                    if (!remote_subdirs.containsKey(lsd.getName()))
                        sftpChannel.mkdir((rsd = new RemoteFile(remote_dir.full_name, lsd.getName(), true)).full_name);
                    else rsd = remote_subdirs.get(lsd.getName());
                    SynchronizeSubDirsR(lsd, rsd);
                }
            }
        }
        if (local_files != null) {
            for (File lf : local_files) {
                RemoteFile rf = null;
                if (!remote_files.containsKey(lf.getName())) {
                    rf = new RemoteFile(remote_dir.full_name, lf.getName());
                    putSingleFile(lf, rf);
                } else {
                    rf = remote_files.get(lf.getName());
                    if (lf.lastModified() > rf.updateTime) {
                        putSingleFile(lf, rf);
                    }
                }
            }
        }
    }
    public void RMDIR(String dir) throws Exception {
        if (!dir.isEmpty() && !dir.equals("/") && !dir.equals("\\") && !dir.equals("*")) {
            ShellCommand("rm -rf " + Utils.DQuotes(dir));
        } else throw new PassException("Недопустимый путь для удаления папки " + Utils.DQuotes(dir));
    }
    //скорее всего,временные методы. они есть в UserConnection, при удаленном запуске тестирования
    //--------------------------------------------------------------------------------
    public void MKDIR(RemoteFile dir) throws Exception {
        if (!Exists(dir)) sftpChannel.mkdir(dir.full_name);
    }
    //--
    public Pair<RemoteFile, RemoteFile> performScript(RemoteFile directory, String... commands) throws Exception {
        RemoteFile script_file = new RemoteFile(directory.full_name, Constants.script);
        RemoteFile out = new RemoteFile(directory.full_name, Constants.out_file);
        RemoteFile err = new RemoteFile(directory.full_name, Constants.err_file);
        //
        Vector<RemoteFile> files = new Vector<>();
        files.add(script_file);
        files.add(out);
        files.add(err);
        for (RemoteFile file : files) {
            if (Exists(file))
                sftpChannel.rm(file.full_name);
        }
        //--
        writeToFile(String.join("\n", commands), script_file);
        //--
        ShellCommand("cd " + Utils.DQuotes(directory.full_name),
                script_file.full_name + " 1>" + Constants.out_file + " 2>" + Constants.err_file);
        return new Pair<>(out, err);
    }
    //--
    //--сам себя
    public boolean Exists(String file_full_name) throws Exception {
        try {
            sftpChannel.stat(file_full_name);
            return true;
        } catch (SftpException e) {
            if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
                // file doesn't exist
                return false;
            } else {
                // something else went wrong
                throw e;
            }
        }
    }
    public boolean Exists(RemoteFile file) throws Exception {
        return Exists(file.full_name);
    }
}


