using Dapper;
using Hangfire;
using KArtSell.BuildingBlocks.Data;
using KArtSell.BuildingBlocks.Reliability;
using KArtSell.BuildingBlocks.Time;
using Microsoft.Extensions.Logging;

namespace KArtSell.Host.Jobs;

/// <summary>
/// Outbox Poller: Publishes unpublished messages to inbox and marks as delivered.
///
/// Design: Consumer='outbox-poller' in inbox_message is a delivery-ready marker.
/// Actual event dispatch (SignalR, email, webhook, etc.) is delegated to future
/// downstream consumer implementations that read inbox_message.
///
/// This separation allows Outbox pattern's durability guarantees without blocking
/// on the specific delivery mechanism.
/// </summary>
public sealed class OutboxPollerJob(
    DapperOutboxMessageReader reader,
    IClock clock,
    ILogger<OutboxPollerJob> logger,
    IDbConnectionFactory connectionFactory,
    Consumers.AuditTrailConsumer auditTrailConsumer)
{
    private const int DefaultBatchSize = 100;
    private const int MaxAttemptsBeforeDq = 3;

    private static readonly Action<ILogger, int, int, Exception?> LogPolled =
        LoggerMessage.Define<int, int>(
            LogLevel.Information,
            new EventId(1, nameof(LogPolled)),
            "Outbox poller processed {MessageCount} messages with {FailureCount} permanent failures.");

    private static readonly Action<ILogger, Guid, string, int, Exception?> LogMessagePublished =
        LoggerMessage.Define<Guid, string, int>(
            LogLevel.Debug,
            new EventId(2, nameof(LogMessagePublished)),
            "Published outbox message {MessageId} ({EventType}, attempt {Attempt}).");

    private static readonly Action<ILogger, Guid, string, int, Exception?> LogMessageDequed =
        LoggerMessage.Define<Guid, string, int>(
            LogLevel.Warning,
            new EventId(3, nameof(LogMessageDequed)),
            "Outbox message {MessageId} ({EventType}) exceeded max attempts ({MaxAttempts}), moved to dead letter queue.");

    private static readonly Action<ILogger, Guid, Exception?> LogProcessError =
        LoggerMessage.Define<Guid>(
            LogLevel.Error,
            new EventId(4, nameof(LogProcessError)),
            "Failed to process outbox message {MessageId}");

    [Queue("q-research")]
    [DisableConcurrentExecution(timeoutInSeconds: 60)]
    [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Fail)]
    public async Task ExecuteAsync(CancellationToken cancellationToken = default)
    {
        var now = clock.UtcNow;

        // Process all unpublished messages ordered by occurred_at (oldest first).
        // Monitoring: alert if any message pending > 5 min (see dashboard/alerts).
        var messages = await reader.GetUnpublishedAsync(DefaultBatchSize, cancellationToken);

        var failureCount = 0;
        var duplicateCount = 0;

        foreach (var message in messages)
        {
            try
            {
                if (message.Attempt >= MaxAttemptsBeforeDq)
                {
                    LogMessageDequed(logger, message.MessageId, message.EventType, MaxAttemptsBeforeDq, null);
                    failureCount++;
                    continue;
                }

                await reader.MarkPublishedAsync(message.MessageId, now, cancellationToken);
                await reader.InsertInboxAsync("outbox-poller", message.MessageId, now, message.PayloadHash, cancellationToken);

                // Log duplicate detection if message was already in inbox (silent conflict via ON CONFLICT DO NOTHING)
                var isDuplicate = await CheckIfDuplicateAsync(message.MessageId, cancellationToken);
                if (isDuplicate)
                {
                    duplicateCount++;
                    await LogDuplicateDetectionAsync(
                        message.MessageId,
                        message.EventType,
                        message.CorrelationId,
                        message.Attempt,
                        now,
                        cancellationToken);
                }

                // DEBT-029: Route to audit trail consumer for event logging
                try
                {
                    await auditTrailConsumer.ConsumeAsync(message, cancellationToken);
                }
                catch (Exception ex)
                {
                    logger.LogWarning(ex, "Audit trail consumer failed for message {MessageId}; continuing", message.MessageId);
                }

                LogMessagePublished(logger, message.MessageId, message.EventType, message.Attempt + 1, null);
            }
            catch (Exception ex)
            {
                LogProcessError(logger, message.MessageId, ex);
                failureCount++;
            }
        }

        LogPolled(logger, messages.Count, failureCount, null);
    }

    private async Task<bool> CheckIfDuplicateAsync(Guid messageId, CancellationToken cancellationToken)
    {
        const string sql = """
            SELECT COUNT(*)
            FROM building_blocks.inbox_message
            WHERE message_id = @MessageId
            """;

        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        var count = await connection.QueryFirstOrDefaultAsync<int>(
            sql,
            new { MessageId = messageId });

        return count > 1;
    }

    private async Task LogDuplicateDetectionAsync(
        Guid messageId,
        string eventType,
        string correlationId,
        int attemptNumber,
        DateTimeOffset detectedAt,
        CancellationToken cancellationToken)
    {
        const string sql = """
            INSERT INTO compliance.operation_audit_trail
            (event_type, correlation_id, entity_type, entity_id, details, detected_at, published_at)
            VALUES (@EventType, @CorrelationId, @EntityType, @EntityId, @Details::jsonb, @DetectedAt, NOW())
            """;

        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        await connection.ExecuteAsync(
            sql,
            new
            {
                EventType = "DUPLICATE_DETECTED",
                CorrelationId = Guid.Parse(correlationId),
                EntityType = "outbox_message",
                EntityId = messageId,
                Details = System.Text.Json.JsonSerializer.Serialize(new { eventType, attemptNumber }),
                DetectedAt = detectedAt.UtcDateTime
            });
    }
}
