using System.Net.Sockets;
using MailKit;
using MailKit.Net.Smtp;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MimeKit;
using ScadaLink.Commons.Entities.Notifications;
using ScadaLink.Commons.Interfaces.Repositories;
using ScadaLink.Commons.Types.Enums;
using ScadaLink.NotificationService;

namespace ScadaLink.NotificationOutbox.Delivery;

/// <summary>
/// Task 12: Email channel delivery adapter for the central notification outbox.
///
/// Reuses the <see cref="ScadaLink.NotificationService"/> SMTP machinery —
/// <see cref="ISmtpClientWrapper"/>, <see cref="SmtpTlsModeParser"/>,
/// <see cref="OAuth2TokenService"/> and the typed <see cref="SmtpPermanentException"/>.
/// The connect/auth/send/disconnect sequence and error classification mirror
/// <c>NotificationDeliveryService.DeliverAsync</c>; this adapter, however, maps the
/// result to the outbox's three-way <see cref="DeliveryOutcome"/> (Success / Permanent
/// / Transient) rather than the S&amp;F-coupled <c>NotificationResult</c>, which cannot
/// distinguish a permanent failure from a buffered transient one.
/// </summary>
public sealed class EmailNotificationDeliveryAdapter : INotificationDeliveryAdapter
{
    private readonly INotificationRepository _repository;
    private readonly Func<ISmtpClientWrapper> _smtpClientFactory;
    private readonly OAuth2TokenService? _tokenService;
    private readonly ILogger<EmailNotificationDeliveryAdapter> _logger;
    private readonly NotificationOptions _options;

    public EmailNotificationDeliveryAdapter(
        INotificationRepository repository,
        Func<ISmtpClientWrapper> smtpClientFactory,
        ILogger<EmailNotificationDeliveryAdapter> logger,
        OAuth2TokenService? tokenService = null,
        IOptions<NotificationOptions>? options = null)
    {
        _repository = repository;
        _smtpClientFactory = smtpClientFactory;
        _logger = logger;
        _tokenService = tokenService;
        // Mirrors NotificationDeliveryService: NotificationOptions supplies the
        // documented fallback values used when a deployed SmtpConfiguration row
        // leaves a field unset (non-positive).
        _options = options?.Value ?? new NotificationOptions();
    }

    /// <inheritdoc />
    public NotificationType Type => NotificationType.Email;

    /// <inheritdoc />
    public async Task<DeliveryOutcome> DeliverAsync(
        Notification notification, CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(notification);

        var list = await _repository.GetListByNameAsync(notification.ListName, cancellationToken);
        if (list == null)
        {
            return DeliveryOutcome.Permanent(
                $"Notification list '{notification.ListName}' not found");
        }

        var recipients = await _repository.GetRecipientsByListIdAsync(list.Id, cancellationToken);
        if (recipients.Count == 0)
        {
            return DeliveryOutcome.Permanent(
                $"Notification list '{notification.ListName}' has no recipients");
        }

        var smtpConfigs = await _repository.GetAllSmtpConfigurationsAsync(cancellationToken);
        var smtpConfig = smtpConfigs.FirstOrDefault();
        if (smtpConfig == null)
        {
            return DeliveryOutcome.Permanent("No SMTP configuration available");
        }

        // An unknown TLS mode is a configuration error that retrying cannot fix —
        // surface it as a permanent failure (mirrors NS-005 in NotificationDeliveryService).
        SmtpTlsMode tlsMode;
        try
        {
            tlsMode = SmtpTlsModeParser.Parse(smtpConfig.TlsMode);
        }
        catch (ArgumentException ex)
        {
            _logger.LogError(
                "Email notification to list '{List}' has an invalid SMTP TLS mode: {Reason}",
                notification.ListName, ex.Message);
            return DeliveryOutcome.Permanent(ex.Message);
        }

        // A malformed sender or recipient address cannot be fixed by retrying —
        // surface it as a permanent failure (mirrors NS-008).
        var addressError = ValidateAddresses(smtpConfig.FromAddress, recipients);
        if (addressError != null)
        {
            _logger.LogWarning(
                "Email notification to list '{List}' has invalid addresses: {Reason}",
                notification.ListName, addressError);
            return DeliveryOutcome.Permanent(addressError);
        }

        var recipientAddresses = recipients.Select(r => r.EmailAddress).ToList();

        try
        {
            await SendAsync(smtpConfig, tlsMode, recipientAddresses,
                notification.Subject, notification.Body, cancellationToken);

            return DeliveryOutcome.Success(string.Join(", ", recipientAddresses));
        }
        catch (SmtpPermanentException ex)
        {
            // Permanent SMTP failure (5xx) — not retried.
            var detail = ScrubCredentials(ex.Message, smtpConfig.Credentials);
            _logger.LogError(
                "Permanent SMTP failure delivering email to list '{List}': {Detail}",
                notification.ListName, detail);
            return DeliveryOutcome.Permanent(detail);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            // A caller-requested cancellation propagates; it is neither a success
            // nor a delivery failure.
            throw;
        }
        catch (Exception ex) when (IsTransientSmtpError(ex, cancellationToken))
        {
            // Transient SMTP failure (4xx, socket/protocol/timeout) — eligible for retry.
            var detail = ScrubCredentials(ex.Message, smtpConfig.Credentials);
            _logger.LogWarning(
                "Transient SMTP failure delivering email to list '{List}' ({ExceptionType}): {Detail}",
                notification.ListName, ex.GetType().Name, detail);
            return DeliveryOutcome.Transient(detail);
        }
        catch (Exception ex)
        {
            // An unclassified failure — chiefly an OAuth2 token-fetch failure. The
            // outbox treats it as permanent: retrying a broken credential burns
            // token-endpoint calls. (Mirrors the NS-015 default-to-permanent stance.)
            var detail = ScrubCredentials(ex.Message, smtpConfig.Credentials);
            _logger.LogError(
                "Unclassified failure delivering email to list '{List}' ({ExceptionType}): {Detail}",
                notification.ListName, ex.GetType().Name, detail);
            return DeliveryOutcome.Permanent($"Email delivery failed: {detail}");
        }
    }

    /// <summary>
    /// Delivers the plain-text BCC email via SMTP. Mirrors the connect/auth/send/
    /// disconnect sequence of <c>NotificationDeliveryService.DeliverAsync</c>: a
    /// permanent failure surfaces as <see cref="SmtpPermanentException"/>; transient
    /// failures propagate for the caller's classifier; the connection is always torn
    /// down in the finally block.
    /// </summary>
    private async Task SendAsync(
        SmtpConfiguration config,
        SmtpTlsMode tlsMode,
        IReadOnlyList<string> bccAddresses,
        string subject,
        string body,
        CancellationToken cancellationToken)
    {
        // Create exactly one client and dispose the one actually used (NS-004).
        var smtp = _smtpClientFactory();
        using var disposable = smtp as IDisposable;

        try
        {
            var timeoutSeconds = config.ConnectionTimeoutSeconds > 0
                ? config.ConnectionTimeoutSeconds
                : _options.ConnectionTimeoutSeconds;
            await smtp.ConnectAsync(
                config.Host, config.Port, tlsMode, timeoutSeconds, cancellationToken);

            // Resolve credentials (OAuth2 token fetched/cached by the token service).
            var credentials = config.Credentials;
            if (config.AuthType.Equals("oauth2", StringComparison.OrdinalIgnoreCase)
                && _tokenService != null && credentials != null)
            {
                credentials = await _tokenService.GetTokenAsync(credentials, cancellationToken);
            }

            await smtp.AuthenticateAsync(config.AuthType, credentials, cancellationToken);
            await smtp.SendAsync(config.FromAddress, bccAddresses, subject, body, cancellationToken);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            // A deliberate cancellation must propagate, not be misclassified as transient.
            throw;
        }
        catch (Exception ex) when (ClassifySmtpError(ex, cancellationToken) == SmtpErrorClass.Permanent
                                   && ex is not SmtpPermanentException)
        {
            // Permanent SMTP failure (5xx) — surface a typed permanent exception.
            throw new SmtpPermanentException(ex.Message, ex);
        }
        // Transient and SmtpPermanentException propagate unchanged for DeliverAsync's
        // catch filters to classify.
        finally
        {
            // Always tear the connection down, regardless of outcome (NS-010).
            // Disconnect is best-effort: a disconnect failure must not mask the
            // original delivery exception.
            try
            {
                await smtp.DisconnectAsync(cancellationToken);
            }
            catch (Exception disconnectEx)
            {
                _logger.LogDebug(
                    "Ignoring SMTP disconnect failure during cleanup: {Reason}", disconnectEx.Message);
            }
        }
    }

    /// <summary>
    /// Validates the sender and recipient email addresses, returning a human-readable
    /// error string if any is malformed, or null if all parse (mirrors NS-008).
    /// </summary>
    private static string? ValidateAddresses(
        string fromAddress, IReadOnlyList<NotificationRecipient> recipients)
    {
        if (!MailboxAddress.TryParse(fromAddress, out _))
        {
            return $"Invalid sender (from) email address: '{fromAddress}'";
        }

        var invalid = recipients
            .Where(r => !MailboxAddress.TryParse(r.EmailAddress, out _))
            .Select(r => r.EmailAddress)
            .ToList();

        return invalid.Count > 0
            ? $"Invalid recipient email address(es): {string.Join(", ", invalid)}"
            : null;
    }

    private enum SmtpErrorClass
    {
        /// <summary>Cancellation or an unrecognised exception — caller decides.</summary>
        Unknown,
        /// <summary>Retryable failure (4xx, connection/socket/protocol error, timeout).</summary>
        Transient,
        /// <summary>Non-retryable failure (5xx) — must not be retried.</summary>
        Permanent,
    }

    /// <summary>
    /// Classifies an SMTP failure using MailKit's typed exceptions and the numeric
    /// <see cref="SmtpStatusCode"/> rather than locale-dependent substring matching
    /// (mirrors NS-002/NS-003 in <c>NotificationDeliveryService</c>).
    /// </summary>
    private static SmtpErrorClass ClassifySmtpError(Exception ex, CancellationToken cancellationToken)
    {
        if (ex is OperationCanceledException && cancellationToken.IsCancellationRequested)
        {
            return SmtpErrorClass.Unknown;
        }

        if (ex is SmtpCommandException command)
        {
            var code = (int)command.StatusCode;
            if (code >= 400 && code < 500)
            {
                return SmtpErrorClass.Transient;
            }

            if (code >= 500 && code < 600)
            {
                return SmtpErrorClass.Permanent;
            }

            return SmtpErrorClass.Unknown;
        }

        if (ex is SmtpProtocolException
            or ServiceNotConnectedException
            or SocketException
            or TimeoutException)
        {
            return SmtpErrorClass.Transient;
        }

        return SmtpErrorClass.Unknown;
    }

    private static bool IsTransientSmtpError(Exception ex, CancellationToken cancellationToken)
    {
        return ClassifySmtpError(ex, cancellationToken) == SmtpErrorClass.Transient;
    }

    /// <summary>
    /// Masks SMTP credential secrets out of free text (typically an SMTP server's
    /// exception message) before it is logged or stored. Mirrors
    /// <c>NotificationService.CredentialRedactor</c>, which is internal to that
    /// project and so cannot be referenced here.
    /// </summary>
    private static string ScrubCredentials(string? text, string? credentials)
    {
        if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(credentials))
        {
            return text ?? string.Empty;
        }

        var result = text;

        // Mask each colon-delimited component (user, password, tenant, clientId,
        // clientSecret) and the whole packed string. Longest first so a component
        // that is a substring of another is still fully masked.
        var parts = credentials.Split(':')
            .Where(p => p.Length >= 4)
            .Append(credentials)
            .Distinct()
            .OrderByDescending(p => p.Length);

        foreach (var part in parts)
        {
            result = result.Replace(part, "***REDACTED***", StringComparison.Ordinal);
        }

        return result;
    }
}
