using System.Net.Sockets;
using System.Text.Json;
using MailKit;
using MailKit.Net.Smtp;
using Microsoft.Extensions.Logging;
using MimeKit;
using ScadaLink.Commons.Entities.Notifications;
using ScadaLink.Commons.Interfaces.Repositories;
using ScadaLink.Commons.Interfaces.Services;
using ScadaLink.Commons.Types.Enums;
using ScadaLink.StoreAndForward;

namespace ScadaLink.NotificationService;

/// <summary>
/// WP-11: Notification delivery via SMTP.
/// WP-12: Error classification and S&amp;F integration.
/// Transient: connection refused, timeout, SMTP 4xx → hand to S&amp;F.
/// Permanent: SMTP 5xx → returned to script.
/// </summary>
public class NotificationDeliveryService : INotificationDeliveryService
{
    private readonly INotificationRepository _repository;
    private readonly Func<ISmtpClientWrapper> _smtpClientFactory;
    private readonly OAuth2TokenService? _tokenService;
    private readonly StoreAndForwardService? _storeAndForward;
    private readonly ILogger<NotificationDeliveryService> _logger;

    public NotificationDeliveryService(
        INotificationRepository repository,
        Func<ISmtpClientWrapper> smtpClientFactory,
        ILogger<NotificationDeliveryService> logger,
        OAuth2TokenService? tokenService = null,
        StoreAndForwardService? storeAndForward = null)
    {
        _repository = repository;
        _smtpClientFactory = smtpClientFactory;
        _logger = logger;
        _tokenService = tokenService;
        _storeAndForward = storeAndForward;
    }

    /// <summary>
    /// Sends a notification to a named list. BCC delivery, plain text.
    /// </summary>
    public async Task<NotificationResult> SendAsync(
        string listName,
        string subject,
        string message,
        string? originInstanceName = null,
        CancellationToken cancellationToken = default)
    {
        var list = await _repository.GetListByNameAsync(listName, cancellationToken);
        if (list == null)
        {
            return new NotificationResult(false, $"Notification list '{listName}' not found");
        }

        var recipients = await _repository.GetRecipientsByListIdAsync(list.Id, cancellationToken);
        if (recipients.Count == 0)
        {
            return new NotificationResult(false, $"Notification list '{listName}' has no recipients");
        }

        var smtpConfigs = await _repository.GetAllSmtpConfigurationsAsync(cancellationToken);
        var smtpConfig = smtpConfigs.FirstOrDefault();
        if (smtpConfig == null)
        {
            return new NotificationResult(false, "No SMTP configuration available");
        }

        // NS-005: validate the configured TLS mode up front — an unknown value is a
        // configuration error and must surface as a clean result, not a silent
        // fallback to opportunistic TLS negotiation.
        try
        {
            SmtpTlsModeParser.Parse(smtpConfig.TlsMode);
        }
        catch (ArgumentException ex)
        {
            _logger.LogError("Invalid SMTP TLS mode for list {List}: {Reason}", listName, ex.Message);
            return new NotificationResult(false, ex.Message);
        }

        // NS-008: validate every email address before attempting delivery. A single
        // malformed address previously caused MailboxAddress.Parse to throw a
        // ParseException that escaped SendAsync unhandled; it must instead produce a
        // clean NotificationResult the calling script can handle.
        var addressError = ValidateAddresses(smtpConfig.FromAddress, recipients);
        if (addressError != null)
        {
            _logger.LogWarning("Notification to list {List} has invalid addresses: {Reason}", listName, addressError);
            return new NotificationResult(false, addressError);
        }

        try
        {
            await DeliverAsync(smtpConfig, recipients, subject, message, cancellationToken);
            return new NotificationResult(true, null);
        }
        catch (SmtpPermanentException ex)
        {
            // WP-12: Permanent SMTP failure — returned to script.
            // NS-009: scrub credential fragments out of the server-supplied message
            // before logging or returning it.
            var detail = CredentialRedactor.Scrub(ex.Message, smtpConfig.Credentials);
            _logger.LogError(
                "Permanent SMTP failure sending to list {List}: {Detail}", listName, detail);
            return new NotificationResult(false, $"Permanent SMTP error: {detail}");
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            // NS-002: a caller-requested cancellation propagates; it is not buffered.
            throw;
        }
        catch (Exception ex) when (IsTransientSmtpError(ex, cancellationToken))
        {
            // WP-12: Transient SMTP failure — hand to S&F.
            // NS-009: scrub credential fragments before logging.
            _logger.LogWarning(
                "Transient SMTP failure sending to list {List} ({ExceptionType}): {Detail}; buffering for retry",
                listName, ex.GetType().Name, CredentialRedactor.Scrub(ex.Message, smtpConfig.Credentials));

            if (_storeAndForward == null)
            {
                return new NotificationResult(false, "Transient SMTP error and store-and-forward not available");
            }

            var payload = JsonSerializer.Serialize(new
            {
                ListName = listName,
                Subject = subject,
                Message = message
            });

            // attemptImmediateDelivery: false — DeliverAsync was already attempted
            // above; letting EnqueueAsync re-invoke the handler would send twice.
            await _storeAndForward.EnqueueAsync(
                StoreAndForwardCategory.Notification,
                listName,
                payload,
                originInstanceName,
                smtpConfig.MaxRetries > 0 ? smtpConfig.MaxRetries : null,
                smtpConfig.RetryDelay > TimeSpan.Zero ? smtpConfig.RetryDelay : null,
                attemptImmediateDelivery: false);

            return new NotificationResult(true, null, WasBuffered: true);
        }
    }

    /// <summary>
    /// WP-11/12: Delivers a buffered notification during a store-and-forward retry
    /// sweep — re-resolves the list, recipients and SMTP config and re-attempts
    /// delivery. Returns true on success, false on permanent failure (the message
    /// is parked); throws on a transient failure so the engine retries.
    /// </summary>
    public async Task<bool> DeliverBufferedAsync(
        StoreAndForwardMessage message, CancellationToken cancellationToken = default)
    {
        var payload = JsonSerializer.Deserialize<BufferedNotification>(message.PayloadJson);
        if (payload == null || string.IsNullOrEmpty(payload.ListName))
        {
            _logger.LogError("Buffered notification message {Id} has an unreadable payload; parking.", message.Id);
            return false;
        }

        var list = await _repository.GetListByNameAsync(payload.ListName, cancellationToken);
        if (list == null)
        {
            _logger.LogError(
                "Buffered notification to list '{List}' cannot be delivered — the list no longer exists; parking.",
                payload.ListName);
            return false;
        }

        var recipients = await _repository.GetRecipientsByListIdAsync(list.Id, cancellationToken);
        if (recipients.Count == 0)
        {
            _logger.LogError("Buffered notification to list '{List}' has no recipients; parking.", payload.ListName);
            return false;
        }

        var smtpConfig = (await _repository.GetAllSmtpConfigurationsAsync(cancellationToken)).FirstOrDefault();
        if (smtpConfig == null)
        {
            _logger.LogError("Buffered notification cannot be delivered — no SMTP configuration available; parking.");
            return false;
        }

        // NS-005: an unknown TLS mode is a configuration error that retrying cannot
        // fix — park the buffered message rather than throwing on every sweep.
        try
        {
            SmtpTlsModeParser.Parse(smtpConfig.TlsMode);
        }
        catch (ArgumentException ex)
        {
            _logger.LogError(
                "Buffered notification to list '{List}' cannot be delivered — {Reason}; parking.",
                payload.ListName, ex.Message);
            return false;
        }

        // NS-008: a malformed address cannot be fixed by retrying — park it.
        var addressError = ValidateAddresses(smtpConfig.FromAddress, recipients);
        if (addressError != null)
        {
            _logger.LogError(
                "Buffered notification to list '{List}' has invalid addresses ({Reason}); parking.",
                payload.ListName, addressError);
            return false;
        }

        try
        {
            await DeliverAsync(smtpConfig, recipients, payload.Subject, payload.Message, cancellationToken);
            return true;
        }
        catch (SmtpPermanentException ex)
        {
            // NS-009: scrub credential fragments out of the message before logging.
            _logger.LogError(
                "Buffered notification to list '{List}' failed permanently ({Detail}); parking.",
                payload.ListName, CredentialRedactor.Scrub(ex.Message, smtpConfig.Credentials));
            return false;
        }
        // Transient SMTP errors propagate out of DeliverAsync — the S&F engine retries.
    }

    private sealed record BufferedNotification(string ListName, string Subject, string Message);

    /// <summary>
    /// NS-007: throttles concurrent SMTP deliveries to the configured
    /// <c>MaxConcurrentConnections</c>. Created lazily from the first SMTP config
    /// seen (one SMTP config is deployed per site, so the limit is stable).
    /// </summary>
    private SemaphoreSlim? _concurrencyLimiter;
    private readonly object _limiterLock = new();

    private SemaphoreSlim GetConcurrencyLimiter(SmtpConfiguration config)
    {
        if (_concurrencyLimiter != null)
        {
            return _concurrencyLimiter;
        }

        lock (_limiterLock)
        {
            // NS-007: a non-positive configured value would make SemaphoreSlim
            // throw; fall back to the design-doc default of 5.
            var max = config.MaxConcurrentConnections > 0 ? config.MaxConcurrentConnections : 5;
            _concurrencyLimiter ??= new SemaphoreSlim(max, max);
            return _concurrencyLimiter;
        }
    }

    /// <summary>
    /// NS-008: Validates the sender and recipient email addresses, returning a
    /// human-readable error string if any is malformed, or null if all parse.
    /// </summary>
    internal 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;
    }

    /// <summary>
    /// Delivers an email via SMTP. Throws on failure (transient errors and
    /// <see cref="SmtpPermanentException"/> propagate; the caller classifies them).
    /// </summary>
    internal async Task DeliverAsync(
        SmtpConfiguration config,
        IReadOnlyList<NotificationRecipient> recipients,
        string subject,
        string body,
        CancellationToken cancellationToken)
    {
        var tlsMode = SmtpTlsModeParser.Parse(config.TlsMode);

        // NS-007: bound the number of concurrent SMTP connections per site.
        var limiter = GetConcurrencyLimiter(config);
        await limiter.WaitAsync(cancellationToken);

        // NS-004: create exactly one client and dispose the one actually used.
        var smtp = _smtpClientFactory();
        using var disposable = smtp as IDisposable;

        try
        {
            // NS-005/NS-007: explicit TLS mode and the configured connection timeout.
            await smtp.ConnectAsync(
                config.Host, config.Port, tlsMode, config.ConnectionTimeoutSeconds, 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)
            {
                var token = await _tokenService.GetTokenAsync(credentials, cancellationToken);
                credentials = token;
            }

            await smtp.AuthenticateAsync(config.AuthType, credentials, cancellationToken);

            var bccAddresses = recipients.Select(r => r.EmailAddress).ToList();
            await smtp.SendAsync(config.FromAddress, bccAddresses, subject, body, cancellationToken);

            await smtp.DisconnectAsync(cancellationToken);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            // NS-002: A deliberately cancelled token must propagate as a cancellation,
            // not be misclassified as a transient SMTP failure and buffered for retry.
            throw;
        }
        catch (Exception ex) when (ClassifySmtpError(ex, cancellationToken) == SmtpErrorClass.Permanent
                                   && ex is not SmtpPermanentException)
        {
            // NS-003: Permanent SMTP failure (5xx) — surface a typed permanent exception.
            throw new SmtpPermanentException(ex.Message, ex);
        }
        // Transient and SmtpPermanentException both propagate unchanged: SendAsync's
        // catch filters (SmtpPermanentException / IsTransientSmtpError) handle them.
        finally
        {
            // NS-007: always release the concurrency slot, even on failure.
            limiter.Release();
        }
    }

    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 be returned to the script.</summary>
        Permanent,
    }

    /// <summary>
    /// NS-002/NS-003: Classifies an SMTP failure using MailKit's typed exceptions and
    /// the numeric <see cref="SmtpStatusCode"/> rather than locale-dependent substring
    /// matching on the exception message. A cancellation requested by the caller is
    /// never treated as a transient SMTP error.
    /// </summary>
    private static SmtpErrorClass ClassifySmtpError(Exception ex, CancellationToken cancellationToken)
    {
        // A deliberate cancellation is not an SMTP error at all.
        if (ex is OperationCanceledException && cancellationToken.IsCancellationRequested)
        {
            return SmtpErrorClass.Unknown;
        }

        // MailKit reports SMTP command failures with the real status code; the
        // SmtpStatusCode enum's underlying value is the numeric SMTP reply code.
        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;
        }

        // Protocol errors, a dropped/unavailable service, socket failures and
        // timeouts are all retryable — the message has not been rejected.
        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>
/// Signals a permanent SMTP failure (5xx) that should not be retried.
/// </summary>
public class SmtpPermanentException : Exception
{
    public SmtpPermanentException(string message, Exception? innerException = null)
        : base(message, innerException) { }
}
