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

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class ForeignExchangeQuoteRepository : IForeignExchangeQuoteRepository
{
    private readonly CashLineErpDbContext _dbContext;

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

    public IReadOnlyCollection<ForeignExchangeQuoteDetailView> GetForeignExchangeQuotes(
        string? currencyCode = null,
        string? baseCurrencyCode = null,
        long? integrationEndpointId = null,
        DateOnly? dateFrom = null,
        DateOnly? dateTo = null)
    {
        if (dateFrom.HasValue && dateTo.HasValue && dateFrom.Value > dateTo.Value)
        {
            throw new InvalidOperationException("FX quote history dateFrom cannot be later than dateTo.");
        }

        var normalizedCurrencyCode = NormalizeOptionalCurrencyCode(currencyCode);
        var normalizedBaseCurrencyCode = NormalizeOptionalCurrencyCode(baseCurrencyCode);

        var endpointMap = _dbContext.IntegrationEndpointRegistryEntries
            .AsNoTracking()
            .ToArray()
            .ToDictionary(entry => entry.Id, entry => entry);

        return _dbContext.Set<ForeignExchangeQuote>()
            .AsNoTracking()
            .ToArray()
            .Where(quote => normalizedCurrencyCode is null || string.Equals(quote.CurrencyCode, normalizedCurrencyCode, StringComparison.OrdinalIgnoreCase))
            .Where(quote => normalizedBaseCurrencyCode is null || string.Equals(quote.BaseCurrencyCode, normalizedBaseCurrencyCode, StringComparison.OrdinalIgnoreCase))
            .Where(quote => !integrationEndpointId.HasValue || quote.IntegrationEndpointId == integrationEndpointId.Value)
            .Where(quote => !dateFrom.HasValue || quote.QuoteDate >= dateFrom.Value)
            .Where(quote => !dateTo.HasValue || quote.QuoteDate <= dateTo.Value)
            .OrderByDescending(quote => quote.QuoteDate)
            .ThenByDescending(quote => quote.Id)
            .Select(quote => Map(quote, endpointMap.GetValueOrDefault(quote.IntegrationEndpointId)))
            .ToArray();
    }

    public ForeignExchangeQuoteDetailView? GetLatestForeignExchangeQuote(
        string currencyCode,
        string? baseCurrencyCode = null,
        DateOnly? asOfDate = null,
        long? integrationEndpointId = null)
    {
        var normalizedCurrencyCode = NormalizeRequiredCurrencyCode(currencyCode);
        var normalizedBaseCurrencyCode = NormalizeOptionalCurrencyCode(baseCurrencyCode) ?? CurrencyConversionContext.Create(_dbContext).BaseCurrencyCode;
        var endpointMap = _dbContext.IntegrationEndpointRegistryEntries
            .AsNoTracking()
            .ToArray()
            .ToDictionary(entry => entry.Id, entry => entry);

        var quote = _dbContext.Set<ForeignExchangeQuote>()
            .AsNoTracking()
            .ToArray()
            .Where(entry => string.Equals(entry.CurrencyCode, normalizedCurrencyCode, StringComparison.OrdinalIgnoreCase))
            .Where(entry => string.Equals(entry.BaseCurrencyCode, normalizedBaseCurrencyCode, StringComparison.OrdinalIgnoreCase))
            .Where(entry => !integrationEndpointId.HasValue || entry.IntegrationEndpointId == integrationEndpointId.Value)
            .Where(entry => !asOfDate.HasValue || entry.QuoteDate <= asOfDate.Value)
            .OrderByDescending(entry => entry.QuoteDate)
            .ThenByDescending(entry => entry.Id)
            .FirstOrDefault();

        return quote is null ? null : Map(quote, endpointMap.GetValueOrDefault(quote.IntegrationEndpointId));
    }

    public IReadOnlyCollection<ForeignExchangeQuoteDetailView> IngestQuotes(long actorUserId, IngestForeignExchangeQuotesCommand command)
    {
        if (command.Quotes.Count == 0)
        {
            throw new InvalidOperationException("FX quote ingestion requires at least one quote line.");
        }

        var endpoint = _dbContext.IntegrationEndpointRegistryEntries.SingleOrDefault(entry => entry.Id == command.IntegrationEndpointId)
            ?? throw new InvalidOperationException("FX quote ingestion requires a valid integration endpoint.");

        if (!endpoint.IsActive || !string.Equals(endpoint.IntegrationProviderCode, "FX_SOURCE", StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("FX quote ingestion requires an active FX_SOURCE integration endpoint.");
        }

        var normalizedBaseCurrencyCode = NormalizeRequiredCurrencyCode(command.BaseCurrencyCode);
        var normalizedLines = command.Quotes
            .Select(line => new
            {
                CurrencyCode = NormalizeRequiredCurrencyCode(line.CurrencyCode),
                line.RateToBase,
                ExternalQuoteReference = NormalizeOptional(line.ExternalQuoteReference),
                Notes = NormalizeOptional(line.Notes)
            })
            .ToArray();

        var duplicateCurrencyInBatch = normalizedLines
            .GroupBy(line => line.CurrencyCode, StringComparer.OrdinalIgnoreCase)
            .FirstOrDefault(group => group.Count() > 1);
        if (duplicateCurrencyInBatch is not null)
        {
            throw new InvalidOperationException($"FX quote ingestion contains duplicate currency '{duplicateCurrencyInBatch.Key}' in the same batch.");
        }

        if (normalizedLines.Any(line => string.Equals(line.CurrencyCode, normalizedBaseCurrencyCode, StringComparison.OrdinalIgnoreCase)))
        {
            throw new InvalidOperationException("FX quote ingestion cannot include the base currency as a quoted currency.");
        }

        foreach (var line in normalizedLines)
        {
            if (line.RateToBase <= 0m)
            {
                throw new InvalidOperationException("FX quote ingestion requires rates greater than zero.");
            }

            var duplicateExists = _dbContext.Set<ForeignExchangeQuote>().Any(entry =>
                entry.IntegrationEndpointId == command.IntegrationEndpointId
                && entry.QuoteDate == command.QuoteDate
                && entry.BaseCurrencyCode == normalizedBaseCurrencyCode
                && entry.CurrencyCode == line.CurrencyCode);

            if (duplicateExists)
            {
                throw new InvalidOperationException(
                    $"FX quote history already contains {line.CurrencyCode}/{normalizedBaseCurrencyCode} for endpoint {endpoint.EndpointCode} on {command.QuoteDate:yyyy-MM-dd}.");
            }
        }

        var startingSequence = (_dbContext.Set<ForeignExchangeQuote>().Max(entry => (long?)entry.Id) ?? 0L) + 1L;
        var createdQuotes = new List<ForeignExchangeQuote>();
        var sequence = startingSequence;
        foreach (var line in normalizedLines)
        {
            var quote = new ForeignExchangeQuote(
                quoteNumber: $"FXQ-{sequence:D6}",
                integrationEndpointId: command.IntegrationEndpointId,
                quoteDate: command.QuoteDate,
                baseCurrencyCode: normalizedBaseCurrencyCode,
                currencyCode: line.CurrencyCode,
                rateToBase: line.RateToBase,
                sourceBatchReference: command.SourceBatchReference,
                externalQuoteReference: line.ExternalQuoteReference,
                notes: line.Notes ?? command.Notes,
                ingestedByUserId: actorUserId);

            createdQuotes.Add(quote);
            sequence += 1L;
        }

        _dbContext.AddRange(createdQuotes);
        _dbContext.SaveChanges();

        return createdQuotes.Select(quote => Map(quote, endpoint)).ToArray();
    }

    private static ForeignExchangeQuoteDetailView Map(ForeignExchangeQuote quote, Domain.Integration.IntegrationEndpointRegistryEntry? endpoint) =>
        new(
            quote.Id,
            quote.QuoteNumber,
            quote.IntegrationEndpointId,
            endpoint?.EndpointCode ?? string.Empty,
            endpoint?.IntegrationProviderCode ?? "FX_SOURCE",
            quote.QuoteDate,
            quote.BaseCurrencyCode,
            quote.CurrencyCode,
            quote.RateToBase,
            quote.SourceBatchReference,
            quote.ExternalQuoteReference,
            quote.Notes,
            quote.IngestedAtUtc,
            quote.IngestedByUserId);

    private static string NormalizeRequiredCurrencyCode(string currencyCode)
    {
        var normalized = NormalizeOptionalCurrencyCode(currencyCode);
        if (normalized is null)
        {
            throw new InvalidOperationException("FX quote currency code is required.");
        }

        return normalized;
    }

    private static string? NormalizeOptionalCurrencyCode(string? currencyCode) =>
        string.IsNullOrWhiteSpace(currencyCode) ? null : currencyCode.Trim().ToUpperInvariant();

    private static string? NormalizeOptional(string? value) =>
        string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
