//  
//   Rebex Sample Code License
// 
//   Copyright 2026, REBEX CR s.r.o.
//   All rights reserved.
//   https://www.rebex.net/
// 
//   Permission to use, copy, modify, and/or distribute this software for any
//   purpose with or without fee is hereby granted.
// 
//   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
//   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
//   OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
//   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
//   HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
//   WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
//   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
//   OTHER DEALINGS IN THE SOFTWARE.
// 

using System;
using System.Text;
using System.IO;

using Rebex.IO;
using Rebex.Net;

namespace Rebex.Samples
{
    /// <summary>
    /// This sample is a simple command line SFTP client.
    /// </summary>
    class ConsoleClient
    {
        public static string ReadLine()
        {
            StringBuilder line = new StringBuilder();
            bool complete = false;

            while (!complete)
            {
                ConsoleKeyInfo key = Console.ReadKey(true);
                switch (key.Key)
                {
                    case ConsoleKey.Enter:
                        complete = true;
                        break;

                    case ConsoleKey.Backspace:
                        if (line.Length > 0)
                        {
                            line = line.Remove(line.Length - 1, 1);
                            Console.Write(key.KeyChar);
                            Console.Write(" ");
                            Console.Write(key.KeyChar);
                        }
                        break;

                    default:
                        if (((int)key.KeyChar >= 0x20) || (key.Key == ConsoleKey.Tab))
                        {
                            line = line.Append(key.KeyChar);
                            Console.Write("*");
                        }
                        break;
                }
            }

            Console.WriteLine();
            return line.ToString();
        }

        private bool _debug = false;
        private string _privateKeyPath = "";

        private void ResponseRead (object sender, SftpResponseReadEventArgs e)
        {
            if (_debug)
            {
                Console.WriteLine ("<- {0}", e.Response);
            }
        }

        private void CommandSent (object sender, SftpCommandSentEventArgs e)
        {
            if (_debug)
            {
                Console.WriteLine ("-> {0}", e.Command);
            }
        }

        private DateTime lastReport = DateTime.MinValue;

        private void TransferProgressChanged (object sender, SftpTransferProgressChangedEventArgs e)
        {
            if (e.TransferState != TransferProgressState.DataBlockProcessed)
                return;

            if ((DateTime.Now - lastReport).TotalSeconds > 1)
            {
                Console.WriteLine (e.BytesTransferred + " bytes.");
                lastReport = DateTime.Now;
            }
        }

        private void AuthenticationRequest(object sender, SshAuthenticationRequestEventArgs e)
        {
            // this is only needed if the server utilizes keyboard-interactive authentication to ask non-trivial questions

            Console.WriteLine("Server: {0}", e.Name);
            Console.WriteLine("Instructions: {0}", e.Instructions);
            foreach (SshAuthenticationRequestItem item in e.Items)
            {
                // display question
                Console.Write(item.Prompt);

                // get answer
                string response;
                if (item.IsSecret)
                    response = ReadLine();
                else
                    response = Console.ReadLine();
                item.Response = response;
            }
        }

        private Sftp ftp;

        public ConsoleClient ()
        {
            // create Sftp object and set response handler
            ftp = new Sftp();
            ftp.CommandSent += CommandSent;
            ftp.ResponseRead += ResponseRead;
            ftp.TransferProgressChanged += TransferProgressChanged;

            // this is only needed if the server utilizes keyboard-interactive authentication to ask non-trivial questions
            ftp.AuthenticationRequest += AuthenticationRequest;
        }

        private void Help ()
        {
            Console.WriteLine("!           get       put");
            Console.WriteLine("?           getr      putr");
            Console.WriteLine("bye         open      mkdir");
            Console.WriteLine("cd          pwd       putdir");
            Console.WriteLine("close       quit      getdir");
            Console.WriteLine("dir         rmdir     rmtree");
            Console.WriteLine("ls          chmod     chown");
            Console.WriteLine("disconnect  user      debug");
            Console.WriteLine("exit        help");
        }

        private void Close ()
        {
            Console.WriteLine ("Disconnecting...");
            ftp.Disconnect();
        }

        private bool Open (string host)
        {
            try
            {

                if (ftp.State != SftpState.Disconnected)
                {
                    Console.WriteLine ("Already connected. Disconnect first.");
                    return false;
                }

                if (host == null || host.Trim().Length == 0)
                {
                    Console.Write ("Hostname: ");
                    host = Console.ReadLine();
                }

                string[] p = host.Split(' ');
                if (p.Length < 0 || p.Length > 3)
                {
                    Console.WriteLine ("Usage: hostname [port] [private key]");
                    return false;
                }

                host = p[0];
                int port = Sftp.DefaultPort;

                switch (p.Length) 
                {
                    case 2:
                        try
                        {
                            port = int.Parse (p[1]);
                        }
                        catch
                        {
                            port = -1;
                        }

                        if (port <= 0 || port > 65535)
                        {
                            port = Sftp.DefaultPort;
                            _privateKeyPath = p[1];
                        }
                        break;
                    case 3:
                        _privateKeyPath = p[2];
                        break;
                }

                ftp.Connect (host, port);

                Console.WriteLine("Fingerprint: " + ftp.Fingerprint.ToString());
                Console.WriteLine("Cipher info: " + ftp.Session.Cipher.ToString());

                return true;
            }
            catch (SftpException e)
            {
                Console.WriteLine (e.Message);
                return false;
            }
        }

        private void Login ()
        {
            Console.Write ("User: ");
            string user = Console.ReadLine();

            if (_privateKeyPath == "")
                Console.Write("Password: ");
            else
                Console.Write("Passphrase for the key file: ");

            string pass = ReadLine();
            if (_privateKeyPath == "") 
            {
                ftp.Login (user, pass);
            } 
            else 
            {
                SshPrivateKey pk = new SshPrivateKey(_privateKeyPath, pass);
                ftp.Login(user, pk);

                pk = null;
                _privateKeyPath = "";
            }
        }

        private void Pwd ()
        {
            if (ftp.State == SftpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            ftp.GetCurrentDirectory();
        }

        private void Cd (string param)
        {
            if (ftp.State == SftpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            ftp.ChangeDirectory (param);
        }

        private void Quit ()
        {
            if (ftp.State != SftpState.Disconnected)
                Close ();

            Console.WriteLine ();
        }

        private void Mkdir (string param)
        {
            if (ftp.State == SftpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            if (param == null || param.Length == 0)
            {
                Console.WriteLine ("Usage: mkdir dirname");
                return;
            }

            ftp.CreateDirectory (param);
        }

        private void Rmdir (string param)
        {
            if (ftp.State == SftpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            if (param == null || param.Length == 0)
            {
                Console.WriteLine ("Usage: rmdir dirname");
                return;
            }

            ftp.RemoveDirectory (param);
        }

        /// <summary>
        /// Removes a whole directory tree. Use with care.
        /// </summary>
        /// <param name="ftp">An instance of Sftp object.</param>
        /// <param name="path">A path to a directory to remove.</param>
        public static void RemoveTree (Sftp ftp, string path)
        {
            // Save the current directory and change it to 'path'.
            string original = ftp.GetCurrentDirectory ();
            ftp.ChangeDirectory (path);

            // Get this list of files and directories
            SftpItemCollection list = ftp.GetList ();
            
            // Delete all files and directories in the list recursively
            for (int i=0; i<list.Count; i++)
            {
                if (list[i].IsDirectory)
                    RemoveTree (ftp, list[i].Name);
                else
                    ftp.DeleteFile (list[i].Name);
            }

            // Go back to the original directory
            ftp.ChangeDirectory (original);

            // Remove the directory that was just cleaned
            ftp.RemoveDirectory (path);
        }

        private void Rmtree (string param)
        {
            if (ftp.State == SftpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            if (param == null || param.Length == 0)
            {
                Console.WriteLine ("Usage: rmtree dirname");
                return;
            }

            RemoveTree (ftp, param);
        }

        private void Dir ()
        {
            if (ftp.State == SftpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            lastReport = DateTime.Now;
            SftpItemCollection list = ftp.GetList ();

            for (int i=0; i<list.Count; i++)
            {
                SftpItem item = list[i];
                if (item.IsLink)
                    Console.Write ("s ");
                else if (item.IsDirectory)
                    Console.Write ("d ");
                else
                    Console.Write ("- ");

                Console.Write (Convert.ToString (((int)item.Permissions)&0x1FF, 8).PadLeft(4,'0') + " ");
                Console.Write (item.LastWriteTime.Value.ToString("u").Substring(0,16));
                Console.Write (item.Length.ToString().PadLeft(10, ' '));
                Console.Write (" {0}", item.Name);
                Console.WriteLine ();
            }
        }

        private void Ls ()
        {
            if (ftp.State == SftpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            lastReport = DateTime.Now;
            SftpItemCollection list = ftp.GetList ();

            for (int i=0; i<list.Count; i++)
            {
                Console.WriteLine (list[i].Name);
            }
        }

        private void Get (string remotePath)
        {
            if (ftp.State == SftpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            if (remotePath == null || remotePath.Length == 0)
            {
                Console.WriteLine ("Usage: get remotePath");
                return;
            }

            try
            {
                lastReport = DateTime.Now;
                ftp.GetFile (remotePath, Path.GetFileName(remotePath));
            }
            catch (Exception e)
            {
                if (e is SftpException)
                    throw;

                Console.WriteLine (e.Message);
            }
        }


        private void GetResume (string remotePath)
        {
            if (ftp.State == SftpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            if (remotePath == null || remotePath.Length == 0)
            {
                Console.WriteLine ("Usage: getr remotePath");
                return;
            }

            FileStream localStream = null;
            try
            {
                // Get local file name corresponding to remote file name.
                string localPath = Path.GetFileName(remotePath);

                // Get the length of remote file. This will throw exception
                // if the file does not exists.
                long remoteLength = ftp.GetFileLength (remotePath);

                // Create or open the local file for writing and seek to the end.
                localStream = File.Open (localPath, FileMode.Append, FileAccess.Write, FileShare.Read);

                // Check lengths too see if the resume makes sense.
                if (localStream.Length == remoteLength)
                {
                    Console.WriteLine ("File has already been downloaded.");
                }
                else if (localStream.Length > remoteLength)
                {
                    Console.WriteLine ("Local file is longer than remote file.");
                }
                else
                {
                    // Display resume info if we are really resuming download.
                    if (localStream.Length > 0)
                        Console.WriteLine ("Download resumed at offset " + localStream.Length + ".");

                    lastReport = DateTime.Now;

                    // Download the file. Start at the position of the length of the local stream.
                    ftp.GetFile (remotePath, localStream, localStream.Length, -1);
                }
            }
            catch (Exception e)
            {
                if (e is SftpException)
                    throw;

                Console.WriteLine (e.Message);
            }
            finally
            {
                // Close the stream if it was created.
                if (localStream != null)
                    localStream.Close ();
            }
        }

        /// <summary>
        /// Uploads a local directory with all files and subdirectories to an SFTP server.
        /// </summary>
        /// <param name="ftp">Instance of an Sftp object.</param>
        /// <param name="localPath">The local path of the root of the directory structure to be uploaded.</param>
        /// <remarks>
        /// <p>
        /// Sftp object must be connected and should have the directory set to the desired path.
        /// </p>
        /// <p>
        /// Method reports information to console. This behaviour should be changed to reflect actual needs.
        /// </p>
        /// </remarks>
        public static void UploadDirectory (Sftp ftp, string localPath)
        {
            // strip ending slashes (GetFileName returns empty string otherwise...)
            localPath = localPath.TrimEnd ('\\','/');

            Console.WriteLine ("Uploading directory: '" + localPath + "'");

            // get the name of the directory and try to create it
            string remotePath = Path.GetFileName (localPath);
            try
            {
                ftp.CreateDirectory (remotePath);
            }
            catch (SftpException e)
            {
                // throw exception if an error other than ProtocolError occurs
                if (e.Status != SftpExceptionStatus.ProtocolError)
                    throw;

                // protocol error - directory probably already exists
                Console.WriteLine (e.Message);
            }
            
            // change the directory
            ftp.ChangeDirectory (remotePath);

            // upload all files from the localPath directory
            string[] files = Directory.GetFiles (localPath);
            for (int i=0; i<files.Length; i++)
            {
                Console.WriteLine ("Uploading file: '" + files[i] + "'");
                try
                {
                    ftp.PutFile (files[i], Path.GetFileName (files[i]));
                }
                catch (SftpException e)
                {
                    // throw exception if an error other than ProtocolError occurs
                    if (e.Status != SftpExceptionStatus.ProtocolError)
                        throw;

                    // protocol error - report it and continue with the remaining files
                    Console.WriteLine (e.Message);
                }
            }

            // upload all directories from the localPath directory
            string[] dirs = Directory.GetDirectories (localPath);
            for (int i=0; i<dirs.Length; i++)
                UploadDirectory (ftp, dirs[i]);

            ftp.ChangeDirectory ("..");
        }

        private void PutDir (string localPath)
        {
            if (ftp.State == SftpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            if (localPath == null || localPath.Length == 0)
            {
                Console.WriteLine ("Usage: putdir localPath");
                return;
            }

            if (!Directory.Exists (localPath))
            {
                Console.WriteLine ("Path does not exist.");
                return;
            }

            lastReport = DateTime.Now;
            UploadDirectory (ftp, localPath);
        }

        public static void DownloadDirectory (Sftp ftp, string remotePath)
        {
            // strip ending slashes (GetFileName returns empty string otherwise...)
            remotePath = remotePath.TrimEnd ('\\','/');

            Console.WriteLine ("Downloading directory: '" + remotePath + "'");

            // get the name of the directory and try to create it
            string localPath = Path.GetFileName (remotePath);
            try
            {
                Directory.CreateDirectory (localPath);
            }
            catch (Exception e)
            {
                // error - directory probably already exists
                Console.WriteLine (e.Message);
            }
            
            // change the directory
            Directory.SetCurrentDirectory (localPath);
            ftp.ChangeDirectory (remotePath);

            // download all files from the remotePath directory
            SftpItemCollection files = ftp.GetList ();
            for (int i=0; i<files.Count; i++)
            {
                if (files[i].IsFile)
                {
                    Console.WriteLine ("Downloading file: '" + files[i].Name + "'");
                    try
                    {
                        if (!File.Exists (files[i].Name))
                            ftp.GetFile (files[i].Name, files[i].Name);
                    }
                    catch (SftpException e)
                    {
                        // throw exception if an error other than ProtocolError occurs
                        if (e.Status != SftpExceptionStatus.ProtocolError)
                            throw;

                        // protocol error - report it and continue with the remaining files
                        Console.WriteLine (e.Message);
                    }
                }
            }

            // download all directories from the remotePath directory
            for (int i=0; i<files.Count; i++)
            {
                if (files[i].IsDirectory)
                    DownloadDirectory (ftp, files[i].Name);
            }

            Directory.SetCurrentDirectory ("..");
            ftp.ChangeDirectory ("..");
        }

        private void GetDir (string remotePath)
        {
            if (ftp.State == SftpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            if (remotePath == null || remotePath.Length == 0)
            {
                Console.WriteLine ("Usage: getdir remotePath");
                return;
            }

            lastReport = DateTime.Now;
            DownloadDirectory (ftp, remotePath);
        }

        private void ChMod(string param)
        {
            if (ftp.State == SftpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            if (param == null || param.Length == 0)
            {
                Console.WriteLine ("Usage: chmod mode remotePath");
                return;
            }

            string[] pars = param.Split(' ');

            if (pars.Length < 2) 
            {
                Console.WriteLine ("Usage: chmod mode remotePath");
                return;
            }

            try 
            {
                SftpAttributes attr = new SftpAttributes();
                attr.Permissions = (SftpPermissions)Convert.ToInt32(pars[0], 8);
                ftp.SetAttributes(pars[1], attr);
            } 
            catch (Exception e)
            {
                if (e is SftpException)
                    throw;

                Console.WriteLine (e.Message);
            }
        }

        private void ChOwn(string param)
        {
            string usageMessage = "Usage: chown uid.gid remotePath";

            if (ftp.State == SftpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            if (param == null || param.Length == 0)
            {
                Console.WriteLine(usageMessage);
                return;
            }

            string[] pars = param.Split(' ');

            if (pars.Length < 2) 
            {
                Console.WriteLine(usageMessage);
                return;
            }

            string[] parsId = pars[0].Split('.');

            if (parsId.Length < 2) 
            {
                Console.WriteLine(usageMessage);
                return;
            }

            try 
            {
                SftpAttributes attr = new SftpAttributes();
                attr.SetUserAndGroup(Convert.ToInt32(parsId[0]), Convert.ToInt32(parsId[1]));
                ftp.SetAttributes(pars[1], attr);
            } 
            catch (Exception e)
            {
                if (e is SftpException)
                    throw;

                Console.WriteLine (e.Message);
            }
        }

        private void Put (string localPath)
        {
            if (ftp.State == SftpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            if (localPath == null || localPath.Length == 0)
            {
                Console.WriteLine ("Usage: put localPath");
                return;
            }

            try
            {
                lastReport = DateTime.Now;
                ftp.PutFile (localPath, Path.GetFileName(localPath));
            }
            catch (Exception e)
            {
                if (e is SftpException)
                    throw;

                Console.WriteLine (e.Message);
            }
        }

        private void PutResume (string localPath)
        {
            if (ftp.State == SftpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            if (localPath == null || localPath.Length == 0)
            {
                Console.WriteLine ("Usage: putr localPath");
                return;
            }

            FileStream localStream = null;
            try
            {
                // Get local file name corresponding to remote file name.
                string remotePath = Path.GetFileName (localPath);
                
                // Try to get the length of the remote file. If the file
                // does not exist, it will throw exception with status
                // of ProtocolError and response code 550. Ignore this
                // exception and report the others.
                long remoteLength = 0;
                try
                {
                    remoteLength = ftp.GetFileLength (remotePath);
                }
                catch (SftpException e)
                {
                    if (e.Status != SftpExceptionStatus.ProtocolError)
                        throw;
                }

                // Open the local file for reading.
                localStream = File.OpenRead (localPath);

                // Check lengths too see if the resume makes sense.
                if (localStream.Length == remoteLength)
                {
                    Console.WriteLine ("File has already been uploaded.");
                }
                else if (localStream.Length < remoteLength)
                {
                    Console.WriteLine ("Remote file is longer than local file.");
                }
                else
                {
                    // Display resume info if we are really resuming upload.
                    if (remoteLength > 0)
                        Console.WriteLine ("Upload resumed at offset " + remoteLength + ".");
                    
                    lastReport = DateTime.Now;

                    // Upload the file. Start at the position of the length of the remote stream.
                    localStream.Position = remoteLength;
                    ftp.PutFile (localStream, remotePath, remoteLength, -1);
                }
            }
            catch (Exception e)
            {
                if (e is SftpException)
                    throw;

                Console.WriteLine (e.Message);
            }
            finally
            {
                // Close the stream if it was created.
                if (localStream != null)
                    localStream.Close ();
            }
        }

        public void Run ()
        {
            while (true)
            {
                Console.Write ("sftp> ");
                string command = Console.ReadLine().Trim();
                string param = null;
                int i = command.IndexOf (' ');
                if (i > 0)
                {
                    param = command.Substring (i + 1);
                    command = command.Substring (0, i);
                }

                try
                {
                    switch (command.ToLower())
                    {
                        case "!":
                            goto case "quit";
                        case "?":
                            goto case "help";
                        case "bye":
                            goto case "quit";
                        case "exit":
                            goto case "quit";
                        case "disconnect":
                            goto case "close";
                        case "quit":
                            Quit();
                            return;
                        case "close":
                            Close();
                            break;
                        case "open":
                            if (!Open (param)) break;
                            Login ();
                            break;
                        case "help":
                            Help();
                            break;
                        case "cd":
                            Cd (param);
                            break;
                        case "pwd":
                            Pwd();
                            break;
                        case "mkdir":
                            Mkdir (param);
                            break;
                        case "rmdir":
                            Rmdir (param);
                            break;
                        case "rmtree":
                            Rmtree (param);
                            break;
                        case "get":
                            Get (param);
                            break;
                        case "put":
                            Put (param);
                            break;
                        case "getr":
                            GetResume (param);
                            break;
                        case "putr":
                            PutResume (param);
                            break;
                        case "putdir":
                            PutDir (param);
                            break;
                        case "getdir":
                            GetDir (param);
                            break;
                        case "chmod":
                            ChMod(param);
                            break;
                        case "chown":
                            ChOwn(param);
                            break;
                        case "dir":
                            Dir();
                            break;
                        case "ls":
                            if (param != null && param.StartsWith ("-l"))
                                Dir();
                            else
                                Ls();
                            break;
                        case "debug":
                            if (_debug) 
                            {
                                Console.WriteLine("Debug is disabled.");
                                _debug = false;
                            } 
                            else 
                            {
                                Console.WriteLine("Debug is enabled.");
                                _debug = true;
                            }
                            break;
                        default:
                            Console.WriteLine ("Invalid command.");
                            break;
                    }
                }
                catch (SftpException e)
                {
                    Console.WriteLine (e.ToString());
                    if (e.Status != SftpExceptionStatus.ProtocolError && e.Status != SftpExceptionStatus.ConnectionClosed)
                    {
                        ftp.Disconnect();
                    }
                }

            }
        }

        static void Main (string[] args)
        {
            // set the trial license key
            Rebex.Licensing.Key = Rebex.Samples.TrialKey.Key;


            ConsoleClient client = new ConsoleClient ();

            if (args.Length > 0)
            {
                string param = args[0];
                for (int i=1; i<args.Length; i++)
                    param += " " + args[i];

                if (client.Open (param))
                    client.Login ();
            }

            client.Run ();
        }
    }
}
