using System.Text.Json;
using CashLineERP.Domain.Finance;
using CashLineERP.Domain.MasterData;
using Microsoft.EntityFrameworkCore;

namespace CashLineERP.Infrastructure.Persistence.Repositories;

internal sealed class CurrencyConversionContext
{
    private readonly IReadOnlyDictionary<string, CurrencyDefinition> _currencies;
    private readonly IReadOnlyCollection<QuoteDefinition> _quotes;
    private readonly IReadOnlyCollection<OverrideDefinition> _overrides;

    private CurrencyConversionContext(
        string baseCurrencyCode,
        IReadOnlyDictionary<string, CurrencyDefinition> currencies,
        IReadOnlyCollection<QuoteDefinition> quotes,
        IReadOnlyCollection<OverrideDefinition> overrides)
    {
        BaseCurrencyCode = baseCurrencyCode;
        _currencies = currencies;
        _quotes = quotes;
        _overrides = overrides;
    }

    public string BaseCurrencyCode { get; }

    public static CurrencyConversionContext Create(CashLineErpDbContext dbContext)
    {
        var activeCurrencies = dbContext.ReferenceDataEntries
            .AsNoTracking()
            .Where(entry => entry.IsActive)
            .ToArray()
            .Where(entry => string.Equals(entry.ReferenceTypeCode, "CURRENCY", StringComparison.OrdinalIgnoreCase))
            .ToArray();

        if (activeCurrencies.Length == 0)
        {
            return new CurrencyConversionContext(
                "EGP",
                new Dictionary<string, CurrencyDefinition>(StringComparer.OrdinalIgnoreCase)
                {
                    ["EGP"] = new("EGP", 1m)
                },
                [],
                []);
        }

        var metadataByCode = activeCurrencies.ToDictionary(
            entry => NormalizeCurrencyCode(entry.ReferenceCode)
                ?? throw new InvalidOperationException("Active currency reference entry is missing a valid code."),
            ParseMetadata,
            StringComparer.OrdinalIgnoreCase);

        var baseCurrencyCode = metadataByCode
                .Where(entry => entry.Value.IsBaseCurrency)
                .Select(entry => entry.Key)
                .FirstOrDefault()
            ?? activeCurrencies
                .Select(entry => NormalizeCurrencyCode(entry.ReferenceCode))
                .FirstOrDefault(code => string.Equals(code, "EGP", StringComparison.OrdinalIgnoreCase))
            ?? metadataByCode.Keys.First();

        var currencies = new Dictionary<string, CurrencyDefinition>(StringComparer.OrdinalIgnoreCase);
        foreach (var entry in metadataByCode)
        {
            var rateToBase = string.Equals(entry.Key, baseCurrencyCode, StringComparison.OrdinalIgnoreCase)
                ? 1m
                : entry.Value.RateToBase
                    ?? throw new InvalidOperationException(
                        $"Active currency '{entry.Key}' is missing 'rateToBase' metadata for base currency conversion.");

            if (!string.IsNullOrWhiteSpace(entry.Value.BaseCurrencyCode)
                && !string.Equals(entry.Value.BaseCurrencyCode, baseCurrencyCode, StringComparison.OrdinalIgnoreCase))
            {
                throw new InvalidOperationException(
                    $"Active currency '{entry.Key}' declares base currency '{entry.Value.BaseCurrencyCode}', but the resolved base currency is '{baseCurrencyCode}'.");
            }

            currencies[entry.Key] = new CurrencyDefinition(entry.Key, rateToBase);
        }

        var quotes = dbContext.Set<ForeignExchangeQuote>()
            .AsNoTracking()
            .ToArray()
            .Select(quote => new QuoteDefinition(
                quote.BaseCurrencyCode,
                quote.CurrencyCode,
                quote.QuoteDate,
                quote.RateToBase,
                quote.IntegrationEndpointId))
            .ToArray();

        var overrides = dbContext.Set<ForeignExchangeOverride>()
            .AsNoTracking()
            .Where(entry => entry.StatusId == CashLineERP.SharedKernel.Workflow.WorkflowStatusSeedIds.Approved)
            .ToArray()
            .Select(entry => new OverrideDefinition(
                entry.BaseCurrencyCode,
                entry.CurrencyCode,
                entry.EffectiveDate,
                entry.OverrideRateToBase,
                entry.Id))
            .ToArray();

        return new CurrencyConversionContext(baseCurrencyCode, currencies, quotes, overrides);
    }

    public decimal ResolveRateToBase(string? currencyCode, DateOnly? asOfDate = null)
    {
        var normalizedCurrencyCode = NormalizeCurrencyCode(currencyCode) ?? BaseCurrencyCode;
        if (string.Equals(normalizedCurrencyCode, BaseCurrencyCode, StringComparison.OrdinalIgnoreCase))
        {
            return 1m;
        }

        var latestQuote = _quotes
            .Where(quote =>
                string.Equals(quote.BaseCurrencyCode, BaseCurrencyCode, StringComparison.OrdinalIgnoreCase)
                && string.Equals(quote.CurrencyCode, normalizedCurrencyCode, StringComparison.OrdinalIgnoreCase)
                && (!asOfDate.HasValue || quote.QuoteDate <= asOfDate.Value))
            .OrderByDescending(quote => quote.QuoteDate)
            .ThenByDescending(quote => quote.IntegrationEndpointId)
            .FirstOrDefault();

        var latestOverride = _overrides
            .Where(entry =>
                string.Equals(entry.BaseCurrencyCode, BaseCurrencyCode, StringComparison.OrdinalIgnoreCase)
                && string.Equals(entry.CurrencyCode, normalizedCurrencyCode, StringComparison.OrdinalIgnoreCase)
                && (!asOfDate.HasValue || entry.EffectiveDate <= asOfDate.Value))
            .OrderByDescending(entry => entry.EffectiveDate)
            .ThenByDescending(entry => entry.ForeignExchangeOverrideId)
            .FirstOrDefault();

        if (latestOverride is not null && latestQuote is not null)
        {
            return latestOverride.EffectiveDate >= latestQuote.QuoteDate
                ? latestOverride.OverrideRateToBase
                : latestQuote.RateToBase;
        }

        if (latestOverride is not null)
        {
            return latestOverride.OverrideRateToBase;
        }

        if (latestQuote is not null)
        {
            return latestQuote.RateToBase;
        }

        if (_currencies.TryGetValue(normalizedCurrencyCode, out var definition))
        {
            return definition.RateToBase;
        }

        throw new InvalidOperationException($"Currency '{currencyCode}' is not active in the reference-data catalog.");
    }

    public decimal ConvertToBase(string? currencyCode, decimal amount, DateOnly? asOfDate = null) =>
        Math.Round(amount * ResolveRateToBase(currencyCode, asOfDate), 2, MidpointRounding.AwayFromZero);

    public bool HasMixedCurrencies(IEnumerable<string?> currencyCodes)
    {
        var normalizedCodes = currencyCodes
            .Select(NormalizeCurrencyCode)
            .Where(code => !string.IsNullOrWhiteSpace(code))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .Take(2)
            .ToArray();

        return normalizedCodes.Length > 1;
    }

    private static CurrencyMetadata ParseMetadata(ReferenceDataEntry entry)
    {
        if (string.IsNullOrWhiteSpace(entry.AttributesJson))
        {
            return new CurrencyMetadata(null, null, false);
        }

        using var document = JsonDocument.Parse(entry.AttributesJson);
        var root = document.RootElement;

        decimal? rateToBase = null;
        if (root.TryGetProperty("rateToBase", out var rateElement) && rateElement.ValueKind == JsonValueKind.Number)
        {
            rateToBase = rateElement.GetDecimal();
        }

        string? baseCurrencyCode = null;
        if (root.TryGetProperty("baseCurrencyCode", out var baseCurrencyElement) && baseCurrencyElement.ValueKind == JsonValueKind.String)
        {
            baseCurrencyCode = NormalizeCurrencyCode(baseCurrencyElement.GetString());
        }

        var isBaseCurrency = root.TryGetProperty("isBaseCurrency", out var baseFlagElement)
            && baseFlagElement.ValueKind is JsonValueKind.True or JsonValueKind.False
            && baseFlagElement.GetBoolean();

        return new CurrencyMetadata(rateToBase, baseCurrencyCode, isBaseCurrency);
    }

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

    private sealed record CurrencyDefinition(
        string CurrencyCode,
        decimal RateToBase);

    private sealed record CurrencyMetadata(
        decimal? RateToBase,
        string? BaseCurrencyCode,
        bool IsBaseCurrency);

    private sealed record QuoteDefinition(
        string BaseCurrencyCode,
        string CurrencyCode,
        DateOnly QuoteDate,
        decimal RateToBase,
        long IntegrationEndpointId);

    private sealed record OverrideDefinition(
        string BaseCurrencyCode,
        string CurrencyCode,
        DateOnly EffectiveDate,
        decimal OverrideRateToBase,
        long ForeignExchangeOverrideId);
}
