using CashLineERP.Application.Integration;
using CashLineERP.Domain.Integration;
using Microsoft.EntityFrameworkCore;

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class InboxMessageRepository : IInboxMessageRepository
{
    private readonly CashLineErpDbContext _dbContext;

    public InboxMessageRepository(CashLineErpDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public IReadOnlyCollection<InboxMessageDetailView> GetInboxMessages(string? statusCode = null, string? integrationProviderCode = null)
    {
        var normalizedStatusCode = string.IsNullOrWhiteSpace(statusCode) ? null : statusCode.Trim().ToUpperInvariant();
        var normalizedProviderCode = string.IsNullOrWhiteSpace(integrationProviderCode) ? null : integrationProviderCode.Trim().ToUpperInvariant();

        return (
            from message in _dbContext.InboxMessages.AsNoTracking()
            join endpoint in _dbContext.IntegrationEndpointRegistryEntries.AsNoTracking()
                on message.IntegrationEndpointId equals endpoint.Id
            where normalizedStatusCode == null || message.StatusCode == normalizedStatusCode
            where normalizedProviderCode == null || endpoint.IntegrationProviderCode == normalizedProviderCode
            orderby message.StatusCode, message.AvailableAtUtc, message.MessageNumber
            select Map(message, endpoint))
            .ToArray();
    }

    public InboxMessageDetailView? GetInboxMessage(long inboxMessageId)
    {
        var item = (
            from message in _dbContext.InboxMessages.AsNoTracking()
            join endpoint in _dbContext.IntegrationEndpointRegistryEntries.AsNoTracking()
                on message.IntegrationEndpointId equals endpoint.Id
            where message.Id == inboxMessageId
            select Map(message, endpoint))
            .SingleOrDefault();

        return item;
    }

    public InboxMessageDetailView? FindExistingMessage(long integrationEndpointId, string? externalMessageId, string? correlationKey)
    {
        var normalizedExternalMessageId = string.IsNullOrWhiteSpace(externalMessageId) ? null : externalMessageId.Trim();
        var normalizedCorrelationKey = string.IsNullOrWhiteSpace(correlationKey) ? null : correlationKey.Trim();

        var existing = (
            from message in _dbContext.InboxMessages.AsNoTracking()
            join endpoint in _dbContext.IntegrationEndpointRegistryEntries.AsNoTracking()
                on message.IntegrationEndpointId equals endpoint.Id
            where message.IntegrationEndpointId == integrationEndpointId
            where (normalizedExternalMessageId != null && message.ExternalMessageId == normalizedExternalMessageId)
                || (normalizedCorrelationKey != null && message.CorrelationKey == normalizedCorrelationKey)
            orderby message.CreatedAtUtc descending, message.Id descending
            select Map(message, endpoint))
            .FirstOrDefault();

        return existing;
    }

    public IReadOnlyCollection<InboxMessageDetailView> GetPendingMessages(int batchSize, string? integrationProviderCode = null, IReadOnlyCollection<string>? excludedIntegrationProviderCodes = null)
    {
        if (batchSize is < 1 or > 100)
        {
            throw new InvalidOperationException("Inbox process batch size must be between 1 and 100.");
        }

        var normalizedProviderCode = string.IsNullOrWhiteSpace(integrationProviderCode) ? null : integrationProviderCode.Trim().ToUpperInvariant();
        var excludedProviderCodes = excludedIntegrationProviderCodes?
            .Where(code => !string.IsNullOrWhiteSpace(code))
            .Select(code => code.Trim().ToUpperInvariant())
            .Distinct()
            .ToArray();

        var nowUtc = DateTime.UtcNow;

        return (
            from message in _dbContext.InboxMessages.AsNoTracking()
            join endpoint in _dbContext.IntegrationEndpointRegistryEntries.AsNoTracking()
                on message.IntegrationEndpointId equals endpoint.Id
            where (message.StatusCode == "RECEIVED" || message.StatusCode == "RETRY")
                  && message.AvailableAtUtc <= nowUtc
            where normalizedProviderCode == null || endpoint.IntegrationProviderCode == normalizedProviderCode
            where excludedProviderCodes == null || !excludedProviderCodes.Contains(endpoint.IntegrationProviderCode)
            orderby message.AvailableAtUtc, message.Id
            select Map(message, endpoint))
            .Take(batchSize)
            .ToArray();
    }

    public InboxMessageDetailView Receive(ReceiveInboxMessageCommand command)
    {
        var endpoint = _dbContext.IntegrationEndpointRegistryEntries
            .SingleOrDefault(entry => entry.Id == command.IntegrationEndpointId);

        if (endpoint is null)
        {
            throw new InvalidOperationException("Integration endpoint was not found.");
        }

        if (!endpoint.IsActive)
        {
            throw new InvalidOperationException("Only active integration endpoints can receive inbox messages.");
        }

        if (!string.Equals(endpoint.DirectionCode, "INBOUND", StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Inbox messages can only be received against inbound integration endpoints.");
        }

        var normalizedCorrelationKey = string.IsNullOrWhiteSpace(command.CorrelationKey) ? null : command.CorrelationKey.Trim();
        var normalizedExternalMessageId = string.IsNullOrWhiteSpace(command.ExternalMessageId) ? null : command.ExternalMessageId.Trim();

        if (endpoint.IsIdempotencyKeyRequired && normalizedCorrelationKey is null && normalizedExternalMessageId is null)
        {
            throw new InvalidOperationException("This endpoint requires a correlation key or external message ID for idempotent inbox receipt.");
        }

        if (endpoint.IsIdempotencyKeyRequired)
        {
            var duplicateExists = _dbContext.InboxMessages.Any(message =>
                message.IntegrationEndpointId == endpoint.Id
                && ((normalizedCorrelationKey != null && message.CorrelationKey == normalizedCorrelationKey)
                    || (normalizedExternalMessageId != null && message.ExternalMessageId == normalizedExternalMessageId)));

            if (duplicateExists)
            {
                throw new InvalidOperationException("A duplicate inbox message already exists for this endpoint and idempotency key.");
            }
        }

        var nextSequence = (_dbContext.InboxMessages.Max(entry => (long?)entry.Id) ?? 0L)
            + _dbContext.ChangeTracker.Entries<InboxMessage>().Count(entry => entry.State == EntityState.Added)
            + 1L;

        var inboxMessage = new InboxMessage(
            messageNumber: $"INBOX-{nextSequence:D6}",
            integrationEndpointId: endpoint.Id,
            messageType: command.MessageType,
            externalMessageId: normalizedExternalMessageId,
            correlationKey: normalizedCorrelationKey,
            payloadText: command.PayloadText,
            availableAtUtc: DateTime.UtcNow);
        inboxMessage.Touch(command.ActorUserId);

        _dbContext.InboxMessages.Add(inboxMessage);
        _dbContext.SaveChanges();

        return Map(inboxMessage, endpoint);
    }

    public ProcessInboxBatchResultView ProcessPendingMessages(ProcessInboxBatchCommand command)
    {
        var processorCode = command.ProcessorCode?.Trim().ToUpperInvariant();
        if (string.IsNullOrWhiteSpace(processorCode))
        {
            throw new InvalidOperationException("Inbox processor code is required.");
        }

        var messages = GetPendingMessages(command.BatchSize, excludedIntegrationProviderCodes: command.ExcludedIntegrationProviderCodes)
            .Select(view => _dbContext.InboxMessages.Single(message => message.Id == view.InboxMessageId))
            .ToArray();

        foreach (var message in messages)
        {
            message.MarkProcessed(processorCode);
        }

        _dbContext.SaveChanges();

        var endpointLookup = _dbContext.IntegrationEndpointRegistryEntries
            .AsNoTracking()
            .Where(entry => messages.Select(message => message.IntegrationEndpointId).Contains(entry.Id))
            .ToDictionary(entry => entry.Id);

        return new ProcessInboxBatchResultView(
            processorCode,
            command.BatchSize,
            messages.Length,
            messages.Select(message => Map(message, endpointLookup[message.IntegrationEndpointId])).ToArray());
    }

    public InboxMessageDetailView MarkProcessed(long inboxMessageId, string processorCode)
    {
        var message = _dbContext.InboxMessages.SingleOrDefault(entry => entry.Id == inboxMessageId)
            ?? throw new InvalidOperationException("Inbox message was not found.");

        message.MarkProcessed(processorCode);
        _dbContext.SaveChanges();

        var endpoint = _dbContext.IntegrationEndpointRegistryEntries
            .AsNoTracking()
            .Single(entry => entry.Id == message.IntegrationEndpointId);
        return Map(message, endpoint);
    }

    public InboxMessageDetailView ScheduleRetry(long inboxMessageId, string errorText, int delaySeconds)
    {
        if (delaySeconds is < 0 or > 86400)
        {
            throw new InvalidOperationException("Inbox retry delay must be between 0 and 86400 seconds.");
        }

        var message = _dbContext.InboxMessages.SingleOrDefault(entry => entry.Id == inboxMessageId)
            ?? throw new InvalidOperationException("Inbox message was not found.");

        message.ScheduleRetry(errorText, DateTime.UtcNow.AddSeconds(delaySeconds));
        _dbContext.SaveChanges();

        var endpoint = _dbContext.IntegrationEndpointRegistryEntries
            .AsNoTracking()
            .Single(entry => entry.Id == message.IntegrationEndpointId);
        return Map(message, endpoint);
    }

    public InboxMessageDetailView MarkFailed(long inboxMessageId, string errorText)
    {
        var message = _dbContext.InboxMessages.SingleOrDefault(entry => entry.Id == inboxMessageId)
            ?? throw new InvalidOperationException("Inbox message was not found.");

        message.MarkFailed(errorText);
        _dbContext.SaveChanges();

        var endpoint = _dbContext.IntegrationEndpointRegistryEntries
            .AsNoTracking()
            .Single(entry => entry.Id == message.IntegrationEndpointId);
        return Map(message, endpoint);
    }

    public InboxMessageDetailView MoveToDeadLetter(long inboxMessageId, long actorUserId, string reasonText)
    {
        var message = _dbContext.InboxMessages.SingleOrDefault(entry => entry.Id == inboxMessageId);
        if (message is null)
        {
            throw new InvalidOperationException("Inbox message was not found.");
        }

        message.MoveToDeadLetter(reasonText);
        message.Touch(actorUserId);
        _dbContext.SaveChanges();

        var endpoint = _dbContext.IntegrationEndpointRegistryEntries
            .AsNoTracking()
            .Single(entry => entry.Id == message.IntegrationEndpointId);
        return Map(message, endpoint);
    }

    public InboxMessageDetailView Replay(long inboxMessageId, long actorUserId, string replayReasonText, int delaySeconds)
    {
        if (delaySeconds is < 0 or > 86400)
        {
            throw new InvalidOperationException("Dead-letter replay delay must be between 0 and 86400 seconds.");
        }

        var message = _dbContext.InboxMessages.SingleOrDefault(entry => entry.Id == inboxMessageId);
        if (message is null)
        {
            throw new InvalidOperationException("Inbox message was not found.");
        }

        message.Replay(replayReasonText, DateTime.UtcNow.AddSeconds(delaySeconds));
        message.Touch(actorUserId);
        _dbContext.SaveChanges();

        var endpoint = _dbContext.IntegrationEndpointRegistryEntries
            .AsNoTracking()
            .Single(entry => entry.Id == message.IntegrationEndpointId);
        return Map(message, endpoint);
    }

    private static InboxMessageDetailView Map(InboxMessage message, IntegrationEndpointRegistryEntry endpoint) =>
        new(
            message.Id,
            message.MessageNumber,
            message.IntegrationEndpointId,
            endpoint.EndpointCode,
            endpoint.IntegrationProviderCode,
            endpoint.EnvironmentCode,
            endpoint.MaxRetryCount,
            message.MessageType,
            message.ExternalMessageId,
            message.CorrelationKey,
            message.PayloadText,
            message.StatusCode,
            message.AttemptNumber,
            message.AvailableAtUtc,
            message.ProcessedAtUtc,
            message.ProcessedByProcess,
            message.LastErrorText,
            message.CreatedAtUtc,
            message.CreatedByUserId);
}
