using System;
using System.IO;
using System.Reflection;
using System.Collections.Generic;
using System.Threading;
using Renci.SshNet;
using System.Linq;
using Renci.SshNet.Common;
using System.Threading.Tasks;

namespace bdf
{
	public class sftp
    {
        public static ConnectionDetails sshPRODDetails = new ConnectionDetails
        {
            host = "mft.rogers.com",
            ppkFile = "BDF.Embedded.mft_passphr.ppk",       //Add "BDF.Embedded." to filename if embedded
            passPhrase = "Rogers1!",
            username = "PRD_3PTY_BDF",
            //password = "f4H&&5igkEwP",
            port = 50022
        };

        public static ConnectionDetails sshDEVDetails = new ConnectionDetails
        {
            host = "dev.mft.rogers.com",
            ppkFile = "BDF.Embedded.private_passphr.ppk",       //Add "BDF.Embedded." to filename if embedded
            passPhrase = "Rogers1!",
            username = "DEV_APP_BDF",
            //password = "DEV_APP_BDF"
        };

        public class ConnectionDetails
        {
            public Int32 maxAttempts = 5;
            public Int32 timeOutSecs = 30;
            public Int32 retrySecs = 5;
            public Int32 OperationTimeout = 30;
            public Int32 KeepAliveInterval = 15;
            public string ppkFile = "";
            public string passPhrase = "";
            public string username = "";
            public string password = "";
            public string host = "";
            public Int32 port = 22;
        }

        public static Renci.SshNet.ConnectionInfo BuildConnectionInfo(ConnectionDetails cd)
        {
            var auth = new System.Collections.Generic.List<Renci.SshNet.AuthenticationMethod>();
            PrivateKeyFile pkFile = null;

            if (cd.username != "" && cd.ppkFile != "")
            {
                if (cd.ppkFile.Contains("Embedded"))
                {
                    Logger.Log(2, " Processing internal PPK");
                    // Get the assembly where the resource is stored
                    Assembly asm = Assembly.GetExecutingAssembly();
                    Stream stream = Stream.Null;

                    if (asm.GetManifestResourceNames().Contains(cd.ppkFile))
                        // Fully-qualified resource name (namespace + filename)
                        stream = asm.GetManifestResourceStream(cd.ppkFile);
                    else
                    {
                        Logger.Log(0, "EXCEPTION: PPK resource not found.");
                        throw new Exception("Private key file not found. ");
                    }

                    //Console.WriteLine("b - {0} {1}", cd.ppkFile, stream == null);
                    try
                    {
                        if (stream == null || !stream.CanRead)
                            throw new Exception("Private key file not found/not readable. ");
                        else
                            stream.Position = 0;    // reset stream position
                    }
                    catch (Exception e)
                    {
                        Logger.Log(3, "EXCEPTION PPK {0}:{1}", e.Message, e.InnerException.Message);
                    }

                    try
                    {
                        pkFile =
                        string.IsNullOrEmpty(cd.passPhrase)
                        ? new PrivateKeyFile(stream)
                        : new PrivateKeyFile(stream, cd.passPhrase);
                    }
                    catch (Renci.SshNet.Common.SshException e)
                    {
                        throw new Exception("Problems with your internal private key and/or passphrase. " + e.Message);
                    }
                    stream.Close();
                    Logger.Log(5, "  Internal PPK loaded");
                }
                else
                {
                    try
                    {
                        pkFile =
                        string.IsNullOrEmpty(cd.passPhrase)
                        ? new PrivateKeyFile(cd.ppkFile)
                        : new PrivateKeyFile(cd.ppkFile, cd.passPhrase); 
                    }
                    catch (Renci.SshNet.Common.SshException e)
                    {
                        throw new Exception("Problems with your private key and/or passphrase. " + e.Message);
                    }
                }
                if (pkFile != null)
                {
                    auth.Add(new Renci.SshNet.PrivateKeyAuthenticationMethod(cd.username, pkFile)); //preferred
                    Logger.Log(2, "  -added PPK auth method.");
                }

            }

            if (!string.IsNullOrEmpty(cd.password))
            {
                var kb = new Renci.SshNet.KeyboardInteractiveAuthenticationMethod(cd.username);
                kb.AuthenticationPrompt += (s, e) =>
                {
                    foreach (var p in e.Prompts)
                        p.Response = cd.password;
                };
                auth.Add(kb);

                auth.Add(new Renci.SshNet.PasswordAuthenticationMethod(cd.username, cd.password));
                Logger.Log(2, "  -added user/pass auth method.");
            }

            return new Renci.SshNet.ConnectionInfo(cd.host, cd.port, cd.username, auth.ToArray())
            {
                Timeout = System.TimeSpan.FromSeconds(30),
                RetryAttempts = 1
            };
        }

        public static Renci.SshNet.SftpClient TrySftpConnectWithHardTimeout(
            Renci.SshNet.ConnectionInfo connectionInfo,
            int maxAttempts,
            int timeoutSeconds)
        {
            for (int i = 0; i < maxAttempts; i++)
            {

                Renci.SshNet.SftpClient client = null;
                try
                {
                    var task = System.Threading.Tasks.Task.Run(() =>
                    {
                        client = new Renci.SshNet.SftpClient(connectionInfo);

                        // These are still useful, but not sufficient alone
                        client.ConnectionInfo.Timeout =
                            System.TimeSpan.FromSeconds(30);

                        client.KeepAliveInterval =
                            System.TimeSpan.FromSeconds(15);
                        client.Connect();
                    });

                    bool completed =
                        task.Wait(System.TimeSpan.FromSeconds(timeoutSeconds));

                    if (!completed)
                    {
                        // HARD TIMEOUT
                        try
                        {
                            client?.Dispose();
                        }
                        catch
                        {
                        }

                        //return null;
                    }
                    else
                    {
                        return client;
                    }
                }
                catch
                {
                    try
                    {
                        client?.Dispose();
                    }
                    catch { }

                    //return null;
                }

                System.Threading.Thread.Sleep(5000);
            }
            return null;
        }

        public static bool serverExists(string host)
        {
            try
            {
                // 1. DNS resolution
                System.Net.IPAddress[] addresses =
                    System.Net.Dns.GetHostAddresses(host);

                if (addresses == null || addresses.Length == 0)
                    return false;

                // 2. Ping first resolved address
                using (System.Net.NetworkInformation.Ping ping =
                       new System.Net.NetworkInformation.Ping())
                {
                    Logger.Log(1, "Validating server: {0} at {1}", host, addresses[0]);
                    System.Net.NetworkInformation.PingReply reply =
                        ping.Send(addresses[0], 3000);  // 3s timeout

                    return reply != null &&
                           reply.Status ==
                           System.Net.NetworkInformation.IPStatus.Success;
                }
            }
            catch
            {
                return false;
            }
        }

        public static void Upload_DictStream(Dictionary<string,MemoryStream> Dict, string task, ConnectionDetails cd)
        { 
            try
            {
                //using (var sftpClient = CreateClient(host, port, username, password))
                var sftpInfo = BuildConnectionInfo(cd);
                Renci.SshNet.SftpClient sftpClient = null;
                try
                {
                    sftpClient = TrySftpConnectWithHardTimeout(sftpInfo, cd.maxAttempts, cd.retrySecs);
                }
                catch (Renci.SshNet.Common.SshConnectionException e)
                {
                    Logger.Log(1, "  connection exception ({0}) : {1}", "", e.Message);
                }

                if (sftpClient == null)
                {
                    Logger.Log(1, "  null connection exception.  Transfer failed.");
                    return;
                }

                string cpath = "";
                foreach (var msFile in Dict)
                {
                    string filename = "Outbox" + Path.DirectorySeparatorChar + task + Path.DirectorySeparatorChar + msFile.Key;
                    MemoryStream ms = msFile.Value;

                    cpath = EnsureSftpDirectoryExists(sftpClient, filename);
                    // file upload 
                    try
                    {
                        ms.Position = 0;
                        sftpClient.UploadFile(
                            ms,
                            filename,
                            uploaded =>
                            {
                                Logger.Log(2, $"Uploaded {Math.Round((double)uploaded / ms.Length * 100)}% of the file.");
                            });
                    }
                    catch (Renci.SshNet.Common.SshException e)
                    {
                        Logger.Log(0, " SSH upload Exception {0}:{1}", e.Message, e.InnerException.Message);
                    }
                    Logger.Log(0, "  -upload: {0} ({3}KB) to {1}:{2}", filename, cd.host, cd.port, (ms.Length / 1024) + 1);

                }

                //Console.WriteLine("starting dir pull {0}", cpath);
                var files = sftpClient.ListDirectory(cpath).Where(f => f.IsRegularFile &&
                     (f.Name.EndsWith(".pdf", System.StringComparison.OrdinalIgnoreCase) || //pdf
                     f.Name.EndsWith(".zip", System.StringComparison.OrdinalIgnoreCase) || //pdf
                     f.Name.EndsWith(".xlsx", System.StringComparison.OrdinalIgnoreCase)));

                Logger.Log(task, " EFT {0} Folder Contents:",task);
                foreach (var s in files)
                {
                    Logger.Log(task, " => {0} ({1}KB) {2}", s.FullName, (s.Attributes.Size/1024)+1, s.Attributes.LastWriteTime);
                }

                sftpClient.Disconnect();
                Logger.Log("Disconnected");
                try
                {
                    sftpClient?.Dispose();
                }
                catch { }

                return; // success

            }
            catch (Exception e)
            {
                Logger.Log(0, "SFTP exception {0}:{1}", e.Message, e.InnerException.Message);
            }
            
        }


        public static void Upload_Stream(MemoryStream ms, string filename, ConnectionDetails cd)
        {
            try
            {
                //using (var sftpClient = CreateClient(host, port, username, password))
                var sftpInfo = BuildConnectionInfo(cd);
                Renci.SshNet.SftpClient sftpClient = null;
                try
                {
                    sftpClient = TrySftpConnectWithHardTimeout(sftpInfo, cd.maxAttempts, cd.retrySecs);
                }
                catch (Renci.SshNet.Common.SshConnectionException e)
                {
                    Logger.Log(1, "  connection exception ({0}) : {1}", "", e.Message);
                }

                if (sftpClient == null)
                {
                    Logger.Log(1, "  null connection exception.  Transfer failed.");
                    return;
                }

                string cpath = EnsureSftpDirectoryExists(sftpClient, filename);
                // file upload 
                try
                {
                    ms.Position = 0;
                    sftpClient.UploadFile(
                        ms,
                        filename,
                        uploaded =>
                        {
                            Logger.Log(2, $"Uploaded {Math.Round((double)uploaded / ms.Length * 100)}% of the file.");
                        });
                }
                catch (Renci.SshNet.Common.SshException e)
                {
                    Logger.Log(0, " SSH upload Exception {0}:{1}", e.Message, e.InnerException.Message);
                }
                Logger.Log(0, "  -- uploaded: {0} ({3}KB) to {1}:{2}", filename, cd.host, cd.port, (ms.Length / 1024)+1);

                //Console.WriteLine("starting dir pull {0}", cpath);
                var files = sftpClient.ListDirectory(cpath).Where(f => f.IsRegularFile && 
                     (f.Name.EndsWith(".pdf", System.StringComparison.OrdinalIgnoreCase) || //pdf
                     f.Name.EndsWith(".zip", System.StringComparison.OrdinalIgnoreCase) || //pdf
                     f.Name.EndsWith(".xlsx", System.StringComparison.OrdinalIgnoreCase)));

                foreach (var s in files)
                {
                    Logger.Log(1, " => {0} {1}", s.FullName, s.Attributes);
                }

                sftpClient.Disconnect();
                Logger.Log(" SSH Disconnected");
                try
                {
                    sftpClient?.Dispose();
                }
                catch { }

                return; // success

            }
            catch (Exception e)
            {
                Logger.Log(0, "SFTP exception {0}:{1}", e.Message, e.InnerException.Message);
            }

        }

        public static string EnsureSftpDirectoryExists(SftpClient client, string path)
        {
            // The path needs to be absolute for reliable operation across different SFTP servers.
            // Ensure the path starts with a '/' if it's meant to be absolute from the root.
            if (!path.StartsWith(Path.DirectorySeparatorChar.ToString() ))
            {
                path = Path.DirectorySeparatorChar + path;
            }

            // Split the path into components, handling potential leading slash
            var parts = path.Trim(Path.DirectorySeparatorChar).Split(Path.DirectorySeparatorChar);
            string currentPath = Path.DirectorySeparatorChar.ToString();
            Logger.Log(3, "Checking path: {0}", path);

            foreach (var part in parts)
            {
                Logger.Log(4, " checking path part {0}", part);
                if (string.IsNullOrEmpty(part)) continue;
                if (parts[parts.Length-1].ToString() == part) break;        // last part is the filename so skip pit.  arrays are 0-based

                currentPath = currentPath == Path.DirectorySeparatorChar.ToString() ? Path.DirectorySeparatorChar.ToString() + part : currentPath + Path.DirectorySeparatorChar.ToString() + part;
                try
                {
                    // Attempt to get attributes of the current path component
                    var attrs = client.GetAttributes(currentPath);
                    if (!attrs.IsDirectory)
                    {
                        // Handle the case where a file exists with the same name as a directory component
                        //throw new Exception($"A file exists with the same name as the directory component: {currentPath}");
                        Logger.Log(4, " found existing filename {0}", part);
                        break;
                    }
                }
                catch (SftpPathNotFoundException)
                {
                    // If the path is not found, create the directory
                    client.CreateDirectory(currentPath);
                    Logger.Log(0, "Creating SFTP directory: {0}", currentPath);
                    System.Threading.Thread.Sleep(2000);
                }
                catch (Exception ex)
                {
                    // Handle other potential exceptions (permissions, etc.)
                    Logger.Log(0, "Error checking/creating directory {0}: {1}",currentPath,ex.Message);
                    throw; // Re-throw to propagate the error
                }
            }
            return currentPath;
        }

    }
}