using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.NotificationService;
using ZB.MOM.WW.ScadaBridge.NotificationService.Ews;

namespace ZB.MOM.WW.ScadaBridge.NotificationOutbox.Delivery;

/// <summary>
/// Email channel delivery adapter for the central notification outbox.
///
/// Reuses the <see cref="ZB.MOM.WW.ScadaBridge.NotificationService"/> SMTP primitives —
/// <see cref="ISmtpClientWrapper"/>, <see cref="SmtpTlsModeParser"/>,
/// <see cref="OAuth2TokenService"/> and the typed <see cref="SmtpPermanentException"/>.
/// This adapter owns the full connect/auth/send/disconnect sequence and maps the
/// outcome to the outbox's three-way <see cref="DeliveryOutcome"/> (Success / Permanent /
/// Transient) — the canonical central-side email delivery path. The prior
/// site-shaped <c>NotificationDeliveryService</c> was deleted with sites no longer
/// delivering notifications.
/// <para>
/// The configured <c>SmtpConfiguration.Transport</c> selects the submission mechanism:
/// <c>Smtp</c> (the default for every row that stores no transport) takes the SMTP path
/// above; <c>Ews</c> hands the message to <see cref="IEwsMailSender"/> instead — one
/// <c>CreateItem</c> SOAP call to an on-prem Exchange endpoint. List resolution, recipient
/// resolution, configuration selection and address validation are shared by both
/// transports; only TLS mode is SMTP-specific.
/// </para>
/// </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;
    private readonly IEwsMailSender? _ewsMailSender;

    /// <summary>Initializes a new instance of <see cref="EmailNotificationDeliveryAdapter"/>.</summary>
    /// <param name="repository">Repository for resolving notification list recipients.</param>
    /// <param name="smtpClientFactory">Factory that creates a new SMTP client wrapper per delivery attempt.</param>
    /// <param name="logger">Logger instance.</param>
    /// <param name="tokenService">Optional OAuth2 token service for Microsoft 365 Client Credentials auth.</param>
    /// <param name="options">Optional notification options providing documented fallback values.</param>
    /// <param name="ewsMailSender">
    /// Optional EWS mail sender used when the selected configuration row sets the Ews transport.
    /// Registered by <c>AddNotificationService()</c>; a host that omits that call leaves it null,
    /// in which case an Ews row fails permanently rather than silently falling back to SMTP.
    /// </param>
    public EmailNotificationDeliveryAdapter(
        INotificationRepository repository,
        Func<ISmtpClientWrapper> smtpClientFactory,
        ILogger<EmailNotificationDeliveryAdapter> logger,
        OAuth2TokenService? tokenService = null,
        IOptions<NotificationOptions>? options = null,
        IEwsMailSender? ewsMailSender = null)
    {
        _repository = repository;
        _smtpClientFactory = smtpClientFactory;
        _logger = logger;
        _tokenService = tokenService;
        // NotificationOptions supplies the documented fallback values used when a
        // deployed SmtpConfiguration row leaves a field unset (non-positive).
        _options = options?.Value ?? new NotificationOptions();
        _ewsMailSender = ewsMailSender;
    }

    /// <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);
        // Deterministic pick: the lowest-Id row wins regardless of repository order,
        // so every node/attempt agrees on the same configuration (arch-review S7).
        var smtpConfig = smtpConfigs.OrderBy(c => c.Id).FirstOrDefault();
        if (smtpConfig == null)
        {
            return DeliveryOutcome.Permanent("No SMTP configuration available");
        }

        if (smtpConfigs.Count > 1)
        {
            _logger.LogWarning(
                "Multiple SMTP configurations exist; using the lowest-Id row ({Id}). "
                + "Enforce a single row or delete extras.",
                smtpConfig.Id);
        }

        // An unknown transport is a configuration error that retrying cannot fix.
        EmailTransport transport;
        try
        {
            transport = EmailTransportParser.Parse(smtpConfig.Transport);
        }
        catch (ArgumentException ex)
        {
            _logger.LogError(
                "Email notification to list '{List}' has an invalid transport: {Reason}",
                notification.ListName, ex.Message);
            return DeliveryOutcome.Permanent(ex.Message);
        }

        // TLS applies to the SMTP transport only — an EWS row's Port/TlsMode are unused.
        // Parsed here, ahead of address validation, so the SMTP path's check order (and
        // hence which reason surfaces for a row that is invalid in more than one way) is
        // exactly what it was before the transport branch existed.
        var tlsMode = SmtpTlsMode.None;
        if (transport == EmailTransport.Smtp)
        {
            // An unknown TLS mode is a configuration error that retrying cannot fix —
            // surface it as a permanent failure (SMTP TLS validation policy).
            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.
        var addressError = EmailAddressValidator.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
            .Where(r => r.EmailAddress is not null)
            .Select(r => r.EmailAddress!)
            .ToList();

        // Everything above is transport-agnostic; the EWS branch takes over here.
        if (transport == EmailTransport.Ews)
        {
            return await DeliverViaEwsAsync(
                smtpConfig, recipientAddresses, notification, cancellationToken);
        }

        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 = CredentialRedactor.Scrub(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 (SmtpErrorClassifier.IsTransient(ex, cancellationToken))
        {
            // Transient SMTP failure (4xx, socket/protocol/timeout) — eligible for retry.
            var detail = CredentialRedactor.Scrub(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 default-to-permanent stance.)
            var detail = CredentialRedactor.Scrub(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 through Exchange Web Services.
    /// <para>
    /// Every EWS-specific configuration defect (a non-HTTPS or relative endpoint, a non-Basic
    /// auth type, a credential that is not <c>username:password</c>, or a missing sender
    /// registration) is a permanent failure decided before the sender is touched — retrying
    /// cannot fix a misconfigured row, and no half-formed request should reach Exchange.
    /// </para>
    /// </summary>
    private async Task<DeliveryOutcome> DeliverViaEwsAsync(
        SmtpConfiguration config,
        IReadOnlyList<string> recipientAddresses,
        Notification notification,
        CancellationToken cancellationToken)
    {
        if (!Uri.TryCreate(config.Host, UriKind.Absolute, out var endpoint)
            || endpoint.Scheme != Uri.UriSchemeHttps)
        {
            return PermanentConfigError(
                "EWS transport requires an absolute https:// endpoint URL in Host; "
                + $"got '{config.Host}'");
        }

        // Basic is the only credential shape the sender builds an Authorization header for;
        // OAuth2/NTLM are documented follow-ons, not silent fallbacks.
        if (!config.AuthType.Equals("basic", StringComparison.OrdinalIgnoreCase))
        {
            return PermanentConfigError(
                $"EWS transport supports only Basic authentication; got '{config.AuthType}'.");
        }

        // Split on the FIRST colon only: a domain-qualified user name has none, but a
        // password legitimately may contain one.
        var parts = config.Credentials?.Split(':', 2) ?? [];
        if (parts.Length != 2 || parts[0].Length == 0 || parts[1].Length == 0)
        {
            return PermanentConfigError("EWS credentials must be in 'username:password' form.");
        }

        if (_ewsMailSender == null)
        {
            return PermanentConfigError(
                "EWS transport configured but no EWS sender is registered");
        }

        var timeoutSeconds = config.ConnectionTimeoutSeconds > 0
            ? config.ConnectionTimeoutSeconds
            : _options.ConnectionTimeoutSeconds;
        var request = new EwsSendRequest(
            endpoint,
            parts[0],
            parts[1],
            config.FromAddress,
            recipientAddresses,
            notification.Subject,
            notification.Body,
            timeoutSeconds);

        try
        {
            await _ewsMailSender.SendAsync(request, cancellationToken);

            return DeliveryOutcome.Success(string.Join(", ", recipientAddresses));
        }
        catch (EwsPermanentException ex)
        {
            // Auth, wrong URL, schema fault or recipient rejection — not retried.
            var detail = CredentialRedactor.Scrub(ex.Message, config.Credentials);
            _logger.LogError(
                "Permanent EWS failure delivering email to list '{List}': {Detail}",
                notification.ListName, detail);
            return DeliveryOutcome.Permanent(detail);
        }
        catch (EwsTransientException ex)
        {
            // Availability-shaped failure (network/timeout/5xx/server-busy) — retried.
            var detail = CredentialRedactor.Scrub(ex.Message, config.Credentials);
            _logger.LogWarning(
                "Transient EWS failure delivering email to list '{List}': {Detail}",
                notification.ListName, detail);
            return DeliveryOutcome.Transient(detail);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            // A caller-requested cancellation propagates; it is neither a success
            // nor a delivery failure.
            throw;
        }
        catch (Exception ex)
        {
            // Anything the sender did not classify is treated as permanent, mirroring the
            // SMTP path's default-to-permanent stance.
            var detail = CredentialRedactor.Scrub(ex.Message, config.Credentials);
            _logger.LogError(
                "Unclassified failure delivering email via EWS to list '{List}' ({ExceptionType}): {Detail}",
                notification.ListName, ex.GetType().Name, detail);
            return DeliveryOutcome.Permanent($"Email delivery failed: {detail}");
        }

        // Local helper: one log-and-park shape for every EWS configuration defect.
        DeliveryOutcome PermanentConfigError(string reason)
        {
            _logger.LogError(
                "Email notification to list '{List}' has an invalid EWS configuration: {Reason}",
                notification.ListName, reason);
            return DeliveryOutcome.Permanent(reason);
        }
    }

    /// <summary>
    /// Delivers the plain-text BCC email via SMTP. 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.
        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,
                    config.OAuth2Authority,
                    config.OAuth2Scope,
                    cancellationToken);
            }

            // OAuth2 XOAUTH2 requires the user identity (FromAddress)
            // to be sent alongside the access token; an empty user is rejected by M365.
            await smtp.AuthenticateAsync(
                config.AuthType,
                credentials,
                oauth2UserName: config.FromAddress,
                cancellationToken: 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 (SmtpErrorClassifier.Classify(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.
            // 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);
            }
        }
    }

}
