using CashLineERP.Application.Finance;
using CashLineERP.Domain.Finance;
using CashLineERP.Domain.Integration;
using CashLineERP.Domain.Workflow;
using CashLineERP.SharedKernel.Finance;
using CashLineERP.SharedKernel.Workflow;
using Microsoft.EntityFrameworkCore;

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class PaymentChannelConfigurationRepository : IPaymentChannelConfigurationRepository
{
    private readonly CashLineErpDbContext _dbContext;

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

    public IReadOnlyCollection<PaymentChannelConfigurationDetailView> GetPaymentChannels(bool activeOnly = false, string? integrationProviderCode = null, string? channelTypeCode = null, string? usageCode = null)
    {
        var normalizedProviderCode = string.IsNullOrWhiteSpace(integrationProviderCode) ? null : PaymentChannelCatalog.NormalizeIntegrationProviderCode(integrationProviderCode);
        var normalizedChannelTypeCode = string.IsNullOrWhiteSpace(channelTypeCode) ? null : PaymentChannelCatalog.NormalizeChannelTypeCode(channelTypeCode);
        var normalizedUsageCode = string.IsNullOrWhiteSpace(usageCode) ? null : PaymentChannelCatalog.NormalizeUsageCode(usageCode);

        return _dbContext.PaymentChannelConfigurations
            .AsNoTracking()
            .ToArray()
            .Where(entry => !activeOnly || entry.IsActive)
            .Where(entry => normalizedProviderCode is null || string.Equals(entry.IntegrationProviderCode, normalizedProviderCode, StringComparison.OrdinalIgnoreCase))
            .Where(entry => normalizedChannelTypeCode is null || string.Equals(entry.ChannelTypeCode, normalizedChannelTypeCode, StringComparison.OrdinalIgnoreCase))
            .Where(entry => normalizedUsageCode is null || string.Equals(entry.UsageCode, normalizedUsageCode, StringComparison.OrdinalIgnoreCase))
            .OrderBy(entry => entry.IntegrationProviderCode)
            .ThenBy(entry => entry.ChannelTypeCode)
            .ThenBy(entry => entry.ChannelCode)
            .Select(Map)
            .ToArray();
    }

    public PaymentChannelConfigurationDetailView? GetPaymentChannel(long paymentChannelConfigurationId)
    {
        var channel = _dbContext.PaymentChannelConfigurations
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == paymentChannelConfigurationId);

        return channel is null ? null : Map(channel);
    }

    public PaymentChannelConfigurationDetailView Create(CreatePaymentChannelConfigurationCommand command)
    {
        var normalizedProviderCode = PaymentChannelCatalog.NormalizeIntegrationProviderCode(command.IntegrationProviderCode);
        var normalizedChannelTypeCode = PaymentChannelCatalog.NormalizeChannelTypeCode(command.ChannelTypeCode);
        var normalizedUsageCode = PaymentChannelCatalog.NormalizeUsageCode(command.UsageCode);
        var normalizedCurrencyCode = PaymentChannelCatalog.NormalizeCurrencyCode(command.CurrencyCode);

        var outboundEndpoint = ResolveIntegrationEndpoint(command.OutboundIntegrationEndpointId, normalizedProviderCode, "OUTBOUND");
        var inboundEndpointId = command.InboundIntegrationEndpointId;
        if (inboundEndpointId.HasValue)
        {
            _ = ResolveIntegrationEndpoint(inboundEndpointId.Value, normalizedProviderCode, "INBOUND");
        }

        var duplicateExists = _dbContext.PaymentChannelConfigurations
            .AsNoTracking()
            .Any(entry =>
                entry.BranchId == command.BranchId
                && entry.IntegrationProviderCode == normalizedProviderCode
                && entry.ChannelTypeCode == normalizedChannelTypeCode
                && entry.UsageCode == normalizedUsageCode
                && entry.CurrencyCode == normalizedCurrencyCode
                && entry.OutboundIntegrationEndpointId == outboundEndpoint.Id);

        if (duplicateExists)
        {
            throw new InvalidOperationException("Payment channel configuration already exists for the same provider, usage, currency, and outbound endpoint.");
        }

        var nextSequence = (_dbContext.PaymentChannelConfigurations.Max(entry => (long?)entry.Id) ?? 0L) + 1L;
        var channel = new PaymentChannelConfiguration(
            channelCode: $"PCH-{nextSequence:D6}",
            channelName: command.ChannelName,
            branchId: command.BranchId,
            integrationProviderCode: normalizedProviderCode,
            channelTypeCode: normalizedChannelTypeCode,
            usageCode: normalizedUsageCode,
            currencyCode: normalizedCurrencyCode,
            outboundIntegrationEndpointId: outboundEndpoint.Id,
            inboundIntegrationEndpointId: inboundEndpointId,
            supportsCardTokenReferences: command.SupportsCardTokenReferences,
            usesWalletRuleEngine: command.UsesWalletRuleEngine,
            requiresCustomerReference: command.RequiresCustomerReference,
            minimumAmount: command.MinimumAmount,
            maximumAmount: command.MaximumAmount,
            settlementLeadDays: command.SettlementLeadDays,
            notes: command.Notes);

        _dbContext.PaymentChannelConfigurations.Add(channel);
        _dbContext.SaveChanges();
        return GetPaymentChannel(channel.Id)!;
    }

    public PaymentChannelConfigurationDetailView Submit(long paymentChannelConfigurationId, long actorUserId)
    {
        var channel = _dbContext.PaymentChannelConfigurations.Single(entry => entry.Id == paymentChannelConfigurationId);
        channel.Submit(actorUserId);

        var workflowInstance = new WorkflowInstance(
            moduleCode: "FINANCE",
            documentType: "PAYMENT_CHANNEL",
            documentId: channel.Id,
            makerUserId: actorUserId,
            workflowRuleId: 1L);
        workflowInstance.Submit(actorUserId, "Payment channel configuration submitted for checker approval.");

        _dbContext.WorkflowInstances.Add(workflowInstance);
        _dbContext.SaveChanges();
        return GetPaymentChannel(channel.Id)!;
    }

    public PaymentChannelConfigurationDetailView Approve(long paymentChannelConfigurationId, long actorUserId)
    {
        var channel = _dbContext.PaymentChannelConfigurations.Single(entry => entry.Id == paymentChannelConfigurationId);
        channel.Approve(actorUserId);

        var workflowInstance = _dbContext.WorkflowInstances
            .Include(entry => entry.Actions)
            .SingleOrDefault(entry => entry.DocumentType == "PAYMENT_CHANNEL" && entry.DocumentId == paymentChannelConfigurationId);
        workflowInstance?.Approve(actorUserId, "Payment channel configuration approved.");

        _dbContext.SaveChanges();
        return GetPaymentChannel(channel.Id)!;
    }

    public PaymentChannelConfigurationDetailView Activate(long paymentChannelConfigurationId, long actorUserId)
    {
        var channel = _dbContext.PaymentChannelConfigurations.Single(entry => entry.Id == paymentChannelConfigurationId);

        var outboundEndpoint = ResolveIntegrationEndpoint(channel.OutboundIntegrationEndpointId, channel.IntegrationProviderCode, "OUTBOUND");
        if (!outboundEndpoint.IsActive)
        {
            throw new InvalidOperationException("Payment channel activation requires an active outbound integration endpoint.");
        }

        if (channel.InboundIntegrationEndpointId.HasValue)
        {
            var inboundEndpoint = ResolveIntegrationEndpoint(channel.InboundIntegrationEndpointId.Value, channel.IntegrationProviderCode, "INBOUND");
            if (!inboundEndpoint.IsActive)
            {
                throw new InvalidOperationException("Payment channel activation requires an active inbound integration endpoint when callback routing is configured.");
            }
        }

        var conflictExists = _dbContext.PaymentChannelConfigurations
            .AsNoTracking()
            .Any(entry =>
                entry.Id != paymentChannelConfigurationId
                && entry.IsActive
                && entry.BranchId == channel.BranchId
                && entry.IntegrationProviderCode == channel.IntegrationProviderCode
                && entry.ChannelTypeCode == channel.ChannelTypeCode
                && entry.UsageCode == channel.UsageCode
                && entry.CurrencyCode == channel.CurrencyCode);

        if (conflictExists)
        {
            throw new InvalidOperationException("Another active payment channel already exists for the same provider, branch, type, usage, and currency.");
        }

        channel.Activate(actorUserId);
        OutboxEventWriter.Enqueue(
            _dbContext,
            sourceModuleCode: "FINANCE",
            aggregateType: "PAYMENT_CHANNEL",
            aggregateId: channel.Id,
            eventType: "PAYMENT_CHANNEL_ACTIVATED",
            payload: new
            {
                channel.ChannelCode,
                channel.ChannelName,
                channel.IntegrationProviderCode,
                channel.ChannelTypeCode,
                channel.UsageCode,
                channel.CurrencyCode,
                channel.OutboundIntegrationEndpointId,
                channel.InboundIntegrationEndpointId
            },
            actorUserId: actorUserId,
            correlationKey: channel.ChannelCode);

        _dbContext.SaveChanges();
        return GetPaymentChannel(channel.Id)!;
    }

    private IntegrationEndpointRegistryEntry ResolveIntegrationEndpoint(long integrationEndpointId, string expectedProviderCode, string expectedDirectionCode)
    {
        var endpoint = _dbContext.IntegrationEndpointRegistryEntries
            .SingleOrDefault(entry => entry.Id == integrationEndpointId)
            ?? throw new InvalidOperationException("Payment channel requires a valid integration endpoint.");

        if (!string.Equals(endpoint.IntegrationProviderCode, expectedProviderCode, StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Payment channel integration endpoint provider must match the configured channel provider.");
        }

        if (!string.Equals(endpoint.DirectionCode, expectedDirectionCode, StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException($"Payment channel requires a {expectedDirectionCode.ToLowerInvariant()} integration endpoint in the selected slot.");
        }

        return endpoint;
    }

    private static PaymentChannelConfigurationDetailView Map(PaymentChannelConfiguration channel) =>
        new(
            channel.Id,
            channel.ChannelCode,
            channel.ChannelName,
            channel.BranchId,
            channel.IntegrationProviderCode,
            channel.ChannelTypeCode,
            channel.UsageCode,
            channel.CurrencyCode,
            channel.OutboundIntegrationEndpointId,
            channel.InboundIntegrationEndpointId,
            channel.SupportsCardTokenReferences,
            channel.UsesWalletRuleEngine,
            channel.RequiresCustomerReference,
            channel.MinimumAmount,
            channel.MaximumAmount,
            channel.SettlementLeadDays,
            WorkflowStatusSeedIds.ToCode(channel.StatusId),
            channel.IsActive,
            channel.Notes,
            channel.SubmittedAtUtc,
            channel.SubmittedByUserId,
            channel.ApprovedAtUtc,
            channel.ApprovedByUserId,
            channel.ActivatedAtUtc,
            channel.ActivatedByUserId);
}
