namespace Swan.Net.Smtp
{
    using System.Threading;
    using System;
    using System.Linq;
    using System.Net;
    using System.Net.Sockets;
    using System.Security;
    using System.Text;
    using System.Net.Security;
    using System.Threading.Tasks;
    using System.Collections.Generic;
    using System.Net.Mail;
    /// 
    /// Represents a basic SMTP client that is capable of submitting messages to an SMTP server.
    /// 
    /// 
    /// The following code explains how to send a simple e-mail.
    /// 
    /// using System.Net.Mail;
    ///  
    /// class Example
    /// {
    ///     static void Main()
    ///     {
    ///         // create a new smtp client using google's smtp server
    ///         var client = new Swan.Net.Smtp.SmtpClient("smtp.gmail.com", 587);
    ///         
    ///         // send an email 
    ///         client.SendMailAsync(
    ///         new MailMessage("sender@test.com", "recipient@test.cm", "Subject", "Body"));
    ///     }
    /// }
    /// 
    /// 
    /// The following code demonstrates how to sent an e-mail using a SmtpSessionState:
    /// 
    /// using Swan.Net.Smtp;
    /// 
    /// class Example
    /// {
    ///     static void Main()
    ///     {
    ///         // create a new smtp client using google's smtp server
    ///         var client = new SmtpClient("smtp.gmail.com", 587);
    ///         
    ///         // create a new session state with a sender address 
    ///         var session = new SmtpSessionState { SenderAddress = "sender@test.com" };
    ///         
    ///         // add a recipient
    ///         session.Recipients.Add("recipient@test.cm");
    ///         
    ///         // send
    ///         client.SendMailAsync(session);
    ///     }
    /// }
    /// 
    /// 
    /// The following code shows how to send an e-mail with an attachment using MimeKit:
    /// 
    /// using MimeKit;
    /// using Swan.Net.Smtp;
    ///  
    /// class Example
    /// {
    ///     static void Main()
    ///     {
    ///         // create a new smtp client using google's smtp server
    ///         var client = new SmtpClient("smtp.gmail.com", 587);
    ///         
    ///         // create a new session state with a sender address 
    ///         var session = new SmtpSessionState { SenderAddress = "sender@test.com" };
    ///         
    ///         // add a recipient
    ///         session.Recipients.Add("recipient@test.cm");
    ///         
    ///         // load a file as an attachment
    ///         var attachment = new MimePart("image", "gif")
    ///         {
    ///             Content = new 
    ///                 MimeContent(File.OpenRead("meme.gif"), ContentEncoding.Default),
    ///             ContentDisposition = 
    ///                 new ContentDisposition(ContentDisposition.Attachment),
    ///             ContentTransferEncoding = ContentEncoding.Base64,
    ///             FileName = Path.GetFileName("meme.gif")
    ///         };
    ///         
    ///         // send
    ///         client.SendMailAsync(session);
    ///     }
    /// }
    /// 
    /// 
    public class SmtpClient
    {
        /// 
        /// Initializes a new instance of the  class.
        /// 
        /// The host.
        /// The port.
        /// host.
        public SmtpClient(string host, int port)
        {
            Host = host ?? throw new ArgumentNullException(nameof(host));
            Port = port;
            ClientHostname = Network.HostName;
        }
        /// 
        /// Gets or sets the credentials. No credentials will be used if set to null.
        /// 
        /// 
        /// The credentials.
        /// 
        public NetworkCredential Credentials { get; set; }
        /// 
        /// Gets the host.
        /// 
        /// 
        /// The host.
        /// 
        public string Host { get; }
        /// 
        /// Gets the port.
        /// 
        /// 
        /// The port.
        /// 
        public int Port { get; }
        /// 
        /// Gets or sets a value indicating whether the SSL is enabled.
        /// If set to false, communication between client and server will not be secured.
        /// 
        /// 
        ///   true if [enable SSL]; otherwise, false.
        /// 
        public bool EnableSsl { get; set; }
        /// 
        /// Gets or sets the name of the client that gets announced to the server.
        /// 
        /// 
        /// The client hostname.
        /// 
        public string ClientHostname { get; set; }
        /// 
        /// Sends an email message asynchronously.
        /// 
        /// The message.
        /// The session identifier.
        /// The callback.
        /// The cancellation token.
        /// 
        /// A task that represents the asynchronous of send email operation.
        /// 
        /// message.
        public Task SendMailAsync(
            MailMessage message,
            string? sessionId = null,
            RemoteCertificateValidationCallback? callback = null,
            CancellationToken cancellationToken = default)
        {
            if (message == null)
                throw new ArgumentNullException(nameof(message));
            var state = new SmtpSessionState
            {
                AuthMode = Credentials == null ? string.Empty : SmtpDefinitions.SmtpAuthMethods.Login,
                ClientHostname = ClientHostname,
                IsChannelSecure = EnableSsl,
                SenderAddress = message.From.Address,
            };
            if (Credentials != null)
            {
                state.Username = Credentials.UserName;
                state.Password = Credentials.Password;
            }
            foreach (var recipient in message.To)
            {
                state.Recipients.Add(recipient.Address);
            }
            state.DataBuffer.AddRange(message.ToMimeMessage().ToArray());
            return SendMailAsync(state, sessionId, callback, cancellationToken);
        }
        /// 
        /// Sends an email message using a session state object.
        /// Credentials, Enable SSL and Client Hostname are NOT taken from the state object but
        /// rather from the properties of this class.
        /// 
        /// The state.
        /// The session identifier.
        /// The callback.
        /// The cancellation token.
        /// 
        /// A task that represents the asynchronous of send email operation.
        /// 
        /// sessionState.
        public Task SendMailAsync(
            SmtpSessionState sessionState,
            string? sessionId = null,
            RemoteCertificateValidationCallback? callback = null,
            CancellationToken cancellationToken = default)
        {
            if (sessionState == null)
                throw new ArgumentNullException(nameof(sessionState));
            return SendMailAsync(new[] { sessionState }, sessionId, callback, cancellationToken);
        }
        /// 
        /// Sends an array of email messages using a session state object.
        /// Credentials, Enable SSL and Client Hostname are NOT taken from the state object but
        /// rather from the properties of this class.
        /// 
        /// The session states.
        /// The session identifier.
        /// The callback.
        /// The cancellation token.
        /// 
        /// A task that represents the asynchronous of send email operation.
        /// 
        /// sessionStates.
        /// Could not upgrade the channel to SSL.
        /// Defines an SMTP Exceptions class.
        public async Task SendMailAsync(
            IEnumerable sessionStates,
            string? sessionId = null,
            RemoteCertificateValidationCallback? callback = null,
            CancellationToken cancellationToken = default)
        {
            if (sessionStates == null)
                throw new ArgumentNullException(nameof(sessionStates));
            using var tcpClient = new TcpClient();
            await tcpClient.ConnectAsync(Host, Port).ConfigureAwait(false);
            using var connection = new Connection(tcpClient, Encoding.UTF8, "\r\n", true, 1000);
            var sender = new SmtpSender(sessionId);
            try
            {
                // Read the greeting message
                sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
                // EHLO 1
                await SendEhlo(sender, connection, cancellationToken).ConfigureAwait(false);
                // STARTTLS
                if (EnableSsl)
                {
                    sender.RequestText = $"{SmtpCommandNames.STARTTLS}";
                    await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
                    sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
                    sender.ValidateReply();
                    if (await connection.UpgradeToSecureAsClientAsync(callback: callback).ConfigureAwait(false) == false)
                        throw new SecurityException("Could not upgrade the channel to SSL.");
                }
                // EHLO 2
                await SendEhlo(sender, connection, cancellationToken).ConfigureAwait(false);
                // AUTH
                if (Credentials != null)
                {
                    var auth = new ConnectionAuth(connection, sender, Credentials);
                    await auth.AuthenticateAsync(cancellationToken).ConfigureAwait(false);
                }
                foreach (var sessionState in sessionStates)
                {
                    {
                        // MAIL FROM
                        sender.RequestText = $"{SmtpCommandNames.MAIL} FROM:<{sessionState.SenderAddress}>";
                        await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
                        sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
                        sender.ValidateReply();
                    }
                    // RCPT TO
                    foreach (var recipient in sessionState.Recipients)
                    {
                        sender.RequestText = $"{SmtpCommandNames.RCPT} TO:<{recipient}>";
                        await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
                        sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
                        sender.ValidateReply();
                    }
                    {
                        // DATA
                        sender.RequestText = $"{SmtpCommandNames.DATA}";
                        await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
                        sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
                        sender.ValidateReply();
                    }
                    {
                        // CONTENT
                        var dataTerminator = sessionState.DataBuffer
                            .Skip(sessionState.DataBuffer.Count - 5)
                            .ToText();
                        sender.RequestText = $"Buffer ({sessionState.DataBuffer.Count} bytes)";
                        await connection.WriteDataAsync(sessionState.DataBuffer.ToArray(), true, cancellationToken).ConfigureAwait(false);
                        if (!dataTerminator.EndsWith(SmtpDefinitions.SmtpDataCommandTerminator))
                            await connection.WriteTextAsync(SmtpDefinitions.SmtpDataCommandTerminator, cancellationToken).ConfigureAwait(false);
                        sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
                        sender.ValidateReply();
                    }
                }
                {
                    // QUIT
                    sender.RequestText = $"{SmtpCommandNames.QUIT}";
                    await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
                    sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
                    sender.ValidateReply();
                }
            }
            catch (Exception ex)
            {
                throw new SmtpException($"Could not send email - Session ID {sessionId}. {ex.Message}\r\n    Last Request: {sender.RequestText}\r\n    Last Reply: {sender.ReplyText}");
            }
        }
        private async Task SendEhlo(SmtpSender sender, Connection connection, CancellationToken cancellationToken)
        {
            sender.RequestText = $"{SmtpCommandNames.EHLO} {ClientHostname}";
            await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
            do
            {
                sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
            }
            while (!sender.IsReplyOk);
            sender.ValidateReply();
        }
        private class ConnectionAuth
        {
            private readonly SmtpSender _sender;
            private readonly Connection _connection;
            private readonly NetworkCredential _credentials;
            public ConnectionAuth(Connection connection, SmtpSender sender, NetworkCredential credentials)
            {
                _connection = connection;
                _sender = sender;
                _credentials = credentials;
            }
            public async Task AuthenticateAsync(CancellationToken ct)
            {
                _sender.RequestText =
                    $"{SmtpCommandNames.AUTH} {SmtpDefinitions.SmtpAuthMethods.Login} {Convert.ToBase64String(Encoding.UTF8.GetBytes(_credentials.UserName))}";
                await _connection.WriteLineAsync(_sender.RequestText, ct).ConfigureAwait(false);
                _sender.ReplyText = await _connection.ReadLineAsync(ct).ConfigureAwait(false);
                _sender.ValidateReply();
                _sender.RequestText = Convert.ToBase64String(Encoding.UTF8.GetBytes(_credentials.Password));
                await _connection.WriteLineAsync(_sender.RequestText, ct).ConfigureAwait(false);
                _sender.ReplyText = await _connection.ReadLineAsync(ct).ConfigureAwait(false);
                _sender.ValidateReply();
            }
        }
    }
}