using System.Text.Json;
using Hangfire;
using Microsoft.Extensions.Logging;
using Npgsql;

namespace Books.Api.Ledger;

/// <summary>
/// Hangfire job that polls the Ledger outbox table and processes transaction events.
/// Events are transformed and can be used to update read models or trigger other domain actions.
/// </summary>
public class LedgerOutboxConsumer(
    NpgsqlDataSource dataSource,
    ILogger<LedgerOutboxConsumer> logger)
{
    private const int BatchSize = 100;

    private const string PollOutboxSql = """
        SELECT id, sequence_number, event_type, aggregate_id, end2end_id, payload, created_at
        FROM outbox
        WHERE processed_at IS NULL
        ORDER BY sequence_number
        LIMIT $1
        FOR UPDATE SKIP LOCKED
        """;

    private const string MarkProcessedSql = """
        UPDATE outbox
        SET processed_at = now()
        WHERE id = $1
        """;

    /// <summary>
    /// Processes pending events from the Ledger outbox.
    /// Called periodically by Hangfire.
    /// </summary>
    [DisableConcurrentExecution(timeoutInSeconds: 30)]
    [AutomaticRetry(Attempts = 3)]
    public async Task ProcessPendingEventsAsync()
    {
        var processedCount = 0;
        var errorCount = 0;

        try
        {
            await using var conn = await dataSource.OpenConnectionAsync();
            await using var tx = await conn.BeginTransactionAsync();

            await using var pollCmd = new NpgsqlCommand(PollOutboxSql, conn, tx)
            {
                Parameters = { new() { Value = BatchSize } }
            };

            await using var reader = await pollCmd.ExecuteReaderAsync();

            var events = new List<OutboxEvent>();
            while (await reader.ReadAsync())
            {
                events.Add(new OutboxEvent
                {
                    Id = reader.GetGuid(0),
                    SequenceNumber = reader.GetInt64(1),
                    EventType = reader.GetString(2),
                    AggregateId = reader.GetString(3),
                    End2EndId = reader.GetString(4),
                    Payload = reader.GetString(5),
                    CreatedAt = reader.GetDateTime(6)
                });
            }

            await reader.CloseAsync();

            foreach (var evt in events)
            {
                try
                {
                    await ProcessEventAsync(evt, conn, tx);
                    await MarkAsProcessedAsync(evt.Id, conn, tx);
                    processedCount++;
                }
                catch (Exception ex)
                {
                    errorCount++;
                    logger.LogError(ex,
                        "Failed to process outbox event {EventId} ({EventType})",
                        evt.Id, evt.EventType);
                    // Continue with other events - failed events will be retried
                }
            }

            await tx.CommitAsync();

            if (processedCount > 0)
            {
                logger.LogInformation(
                    "Processed {ProcessedCount} Ledger outbox events ({ErrorCount} errors)",
                    processedCount, errorCount);
            }
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Error polling Ledger outbox");
            throw;
        }
    }

    private Task ProcessEventAsync(OutboxEvent evt, NpgsqlConnection conn, NpgsqlTransaction tx)
    {
        // Parse the event and take appropriate action
        // Currently just logging - can be extended to emit DDD commands
        return evt.EventType switch
        {
            "transaction.auto" => HandleTransactionAutoAsync(evt),
            "transaction.reserve" => HandleTransactionReserveAsync(evt),
            "transaction.capture" => HandleTransactionCaptureAsync(evt),
            "transaction.void" => HandleTransactionVoidAsync(evt),
            _ => HandleUnknownEventAsync(evt)
        };
    }

    private Task HandleTransactionAutoAsync(OutboxEvent evt)
    {
        var payload = JsonSerializer.Deserialize<TransactionPayload>(evt.Payload);
        logger.LogDebug(
            "Transaction.Auto: {IdempotencyKey} with {LegCount} legs for period {PeriodId}",
            payload?.IdempotencyKey, payload?.Legs?.Count ?? 0, payload?.PeriodId);

        // Future: Emit PostingRecordedCommand to DDD for read model updates
        // var command = new RecordPostingCommand(...);
        // await commandBus.PublishAsync(command);

        return Task.CompletedTask;
    }

    private Task HandleTransactionReserveAsync(OutboxEvent evt)
    {
        logger.LogDebug("Transaction.Reserve: {AggregateId}", evt.AggregateId);
        return Task.CompletedTask;
    }

    private Task HandleTransactionCaptureAsync(OutboxEvent evt)
    {
        logger.LogDebug("Transaction.Capture: {AggregateId}", evt.AggregateId);
        return Task.CompletedTask;
    }

    private Task HandleTransactionVoidAsync(OutboxEvent evt)
    {
        logger.LogDebug("Transaction.Void: {AggregateId}", evt.AggregateId);
        return Task.CompletedTask;
    }

    private Task HandleUnknownEventAsync(OutboxEvent evt)
    {
        logger.LogWarning("Unknown outbox event type: {EventType}", evt.EventType);
        return Task.CompletedTask;
    }

    private static async Task MarkAsProcessedAsync(Guid eventId, NpgsqlConnection conn, NpgsqlTransaction tx)
    {
        await using var cmd = new NpgsqlCommand(MarkProcessedSql, conn, tx)
        {
            Parameters = { new() { Value = eventId } }
        };
        await cmd.ExecuteNonQueryAsync();
    }

    private sealed class OutboxEvent
    {
        public required Guid Id { get; init; }
        public required long SequenceNumber { get; init; }
        public required string EventType { get; init; }
        public required string AggregateId { get; init; }
        public required string End2EndId { get; init; }
        public required string Payload { get; init; }
        public required DateTime CreatedAt { get; init; }
    }

    private sealed class TransactionPayload
    {
        public string? IdempotencyKey { get; init; }
        public string? End2EndId { get; init; }
        public string? TransactionType { get; init; }
        public string? ReferenceId { get; init; }
        public List<LegPayload>? Legs { get; init; }
        public Guid? PeriodId { get; init; }
    }

    private sealed class LegPayload
    {
        public Guid AccountId { get; init; }
        public string? Type { get; init; }
        public decimal Amount { get; init; }
        public string? Currency { get; init; }
    }
}
