using MailKit.Net.Smtp;
using MailKit.Security;
using MimeKit;

namespace ZB.MOM.WW.ScadaBridge.NotificationService;

/// <summary>
/// MailKit-based SMTP client wrapper.
/// Supports OAuth2 Client Credentials (M365) and Basic Auth.
/// BCC delivery, plain text.
/// </summary>
/// <remarks>
/// <para>
/// <b>Lifetime — one wrapper, one delivery.</b>
/// This wrapper owns a SINGLE underlying <see cref="MailKit.Net.Smtp.SmtpClient"/>
/// — it is NOT a connection pool. MailKit's <c>SmtpClient</c> is a single TCP/TLS
/// connection holder and is NOT thread-safe; reusing one across concurrent or
/// back-to-back deliveries without external synchronization is unsafe.
/// </para>
/// <para>
/// The DI registration (<c>AddSingleton&lt;Func&lt;ISmtpClientWrapper&gt;&gt;</c>)
/// is therefore a per-delivery FACTORY, not a singleton wrapper: callers
/// (<see cref="ZB.MOM.WW.ScadaBridge.NotificationOutbox.Delivery.EmailNotificationDeliveryAdapter"/>)
/// invoke the factory at the top of every <c>DeliverAsync</c>, run the
/// connect/authenticate/send/disconnect sequence on the fresh wrapper, and
/// dispose it at the end of the send. Each delivery pays a full TCP+TLS
/// handshake; this is the deliberate, documented cost of avoiding shared
/// connection state. The factory shape exists specifically so a future
/// pooled/synchronized implementation can be slotted in without changing
/// callers — but the current implementation deliberately does NOT pool.
/// </para>
/// <para>
/// Do not reuse one wrapper across deliveries. <see cref="ConnectAsync"/>
/// mutates <c>_client.Timeout</c> per call, and the underlying
/// <c>SmtpClient</c> rejects concurrent send calls — both are latent footguns
/// for any caller tempted to "fix" the factory into a true singleton.
/// </para>
/// </remarks>
public class MailKitSmtpClientWrapper : ISmtpClientWrapper, IDisposable
{
    // ONE SmtpClient per wrapper — see class-level remarks. This is NOT a
    // connection pool. MailKit's SmtpClient holds a single TCP/TLS connection and
    // is not thread-safe; the wrapper is meant for a single connect/auth/send/
    // disconnect cycle per instance, after which it MUST be disposed.
    private readonly SmtpClient _client = new();

    /// <inheritdoc />
    public async Task ConnectAsync(
        string host,
        int port,
        SmtpTlsMode tlsMode,
        int connectionTimeoutSeconds,
        CancellationToken cancellationToken = default)
    {
        // Map the explicit three-state TLS mode onto MailKit's socket
        // options. The old code collapsed everything to a boolean and used
        // SecureSocketOptions.Auto for the non-StartTLS case, which let MailKit
        // opportunistically negotiate TLS even when "None" was configured and
        // gave SSL-on-connect no representation at all.
        var secureSocket = tlsMode switch
        {
            SmtpTlsMode.None => SecureSocketOptions.None,
            SmtpTlsMode.StartTls => SecureSocketOptions.StartTls,
            SmtpTlsMode.Ssl => SecureSocketOptions.SslOnConnect,
            _ => throw new ArgumentOutOfRangeException(nameof(tlsMode), tlsMode, "Unknown TLS mode."),
        };

        // Honour the configured connection timeout. SmtpClient.Timeout is
        // in milliseconds and applies to connect/auth/send operations.
        if (connectionTimeoutSeconds > 0)
        {
            _client.Timeout = connectionTimeoutSeconds * 1000;
        }

        await _client.ConnectAsync(host, port, secureSocket, cancellationToken);
    }

    /// <inheritdoc />
    public async Task AuthenticateAsync(
        string authType,
        string? credentials,
        string? oauth2UserName = null,
        CancellationToken cancellationToken = default)
    {
        // Missing/unparseable credentials and an unrecognised auth type used
        // to make this method silently return and the connection then sent mail
        // unauthenticated — masking a misconfiguration against an open relay and, at
        // worst, sending where authentication was required. Authentication being
        // skipped must never be silent: each of these is a permanent configuration
        // fault, surfaced as SmtpPermanentException so SendAsync returns a clean
        // failure that the central Notification Outbox dispatcher classifies as permanent.
        if (string.IsNullOrEmpty(credentials))
        {
            throw new SmtpPermanentException(
                $"SMTP auth type '{authType}' requires credentials, but none are configured.");
        }

        switch (authType.ToLowerInvariant())
        {
            case "basic":
                var parts = credentials.Split(':', 2);
                if (parts.Length != 2)
                {
                    throw new SmtpPermanentException(
                        "Basic SMTP credentials must be in 'username:password' form.");
                }

                await _client.AuthenticateAsync(parts[0], parts[1], cancellationToken);
                break;

            case "oauth2":
                // The XOAUTH2 SASL initial response embeds a `user=<userName>`
                // field that M365 (and most OAuth2-enabled SMTP relays) require to
                // match the mailbox identity the token was issued for. An empty user
                // gets rejected with `535 5.7.3`. The token (credentials) is
                // pre-fetched by OAuth2TokenService; the user identity is the SMTP
                // From address, threaded through `oauth2UserName`.
                if (string.IsNullOrEmpty(oauth2UserName))
                {
                    throw new SmtpPermanentException(
                        "OAuth2 SMTP auth requires a non-empty user identity " +
                        "(mailbox the access token was issued for); " +
                        "the caller did not pass an oauth2UserName.");
                }

                var oauth2 = new SaslMechanismOAuth2(oauth2UserName, credentials);
                await _client.AuthenticateAsync(oauth2, cancellationToken);
                break;

            default:
                throw new SmtpPermanentException(
                    $"Unsupported SMTP auth type '{authType}'. Expected one of: basic, oauth2.");
        }
    }

    /// <inheritdoc />
    public async Task SendAsync(string from, IEnumerable<string> bccRecipients, string subject, string body, CancellationToken cancellationToken = default)
    {
        var message = new MimeMessage();
        message.From.Add(MailboxAddress.Parse(from));

        foreach (var recipient in bccRecipients)
        {
            message.Bcc.Add(MailboxAddress.Parse(recipient));
        }

        message.Subject = subject;
        message.Body = new TextPart("plain") { Text = body };

        await _client.SendAsync(message, cancellationToken);
    }

    /// <inheritdoc />
    public async Task DisconnectAsync(CancellationToken cancellationToken = default)
    {
        if (_client.IsConnected)
        {
            await _client.DisconnectAsync(true, cancellationToken);
        }
    }

    /// <summary>Disposes the underlying MailKit SMTP client.</summary>
    public void Dispose()
    {
        _client.Dispose();
    }
}
