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

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class WalletRuleRepository : IWalletRuleRepository
{
    private readonly CashLineErpDbContext _dbContext;

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

    public IReadOnlyCollection<WalletRuleDetailView> GetWalletRules(bool activeOnly = false, string? integrationProviderCode = null, string? transactionTypeCode = null)
    {
        var normalizedProviderCode = string.IsNullOrWhiteSpace(integrationProviderCode)
            ? null
            : WalletRuleCatalog.NormalizeIntegrationProviderCode(integrationProviderCode);
        var normalizedTransactionTypeCode = string.IsNullOrWhiteSpace(transactionTypeCode)
            ? null
            : WalletRuleCatalog.NormalizeTransactionTypeCode(transactionTypeCode);

        return _dbContext.WalletRules
            .AsNoTracking()
            .ToArray()
            .Where(rule => !activeOnly || rule.IsActive)
            .Where(rule => normalizedProviderCode is null || string.Equals(rule.IntegrationProviderCode, normalizedProviderCode, StringComparison.OrdinalIgnoreCase))
            .Where(rule => normalizedTransactionTypeCode is null || string.Equals(rule.TransactionTypeCode, normalizedTransactionTypeCode, StringComparison.OrdinalIgnoreCase))
            .OrderByDescending(rule => rule.Priority)
            .ThenBy(rule => rule.RuleCode)
            .Select(Map)
            .ToArray();
    }

    public WalletRuleDetailView? GetWalletRule(long walletRuleId)
    {
        var rule = _dbContext.WalletRules
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == walletRuleId);

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

    public WalletRuleDetailView Create(CreateWalletRuleCommand command)
    {
        AccountingPeriodLockGuard.EnsureOpenPeriod(_dbContext, command.EffectiveFromDate, "wallet rule");

        var normalizedProviderCode = WalletRuleCatalog.NormalizeIntegrationProviderCode(command.IntegrationProviderCode);
        var normalizedPartyTypeCode = WalletRuleCatalog.NormalizePartyTypeCode(command.PartyTypeCode);
        var normalizedTransactionTypeCode = WalletRuleCatalog.NormalizeTransactionTypeCode(command.TransactionTypeCode);
        var normalizedWalletSchemeCode = WalletRuleCatalog.NormalizeWalletSchemeCode(command.WalletSchemeCode);
        var normalizedCurrencyCode = WalletRuleCatalog.NormalizeCurrencyCode(command.CurrencyCode);

        var duplicateExists = _dbContext.WalletRules
            .AsNoTracking()
            .Any(entry =>
                entry.BranchId == command.BranchId
                && entry.PartyTypeCode == normalizedPartyTypeCode
                && entry.TransactionTypeCode == normalizedTransactionTypeCode
                && entry.WalletSchemeCode == normalizedWalletSchemeCode
                && entry.IntegrationProviderCode == normalizedProviderCode
                && entry.CurrencyCode == normalizedCurrencyCode
                && entry.EffectiveFromDate == command.EffectiveFromDate);

        if (duplicateExists)
        {
            throw new InvalidOperationException("Wallet rule already exists for the same provider, scope, and effective-from date.");
        }

        var nextSequence = (_dbContext.WalletRules.Max(entry => (long?)entry.Id) ?? 0L) + 1L;
        var rule = new WalletRule(
            ruleCode: $"WLT-{nextSequence:D6}",
            ruleName: command.RuleName,
            branchId: command.BranchId,
            partyTypeCode: normalizedPartyTypeCode,
            transactionTypeCode: normalizedTransactionTypeCode,
            walletSchemeCode: normalizedWalletSchemeCode,
            integrationProviderCode: normalizedProviderCode,
            currencyCode: normalizedCurrencyCode,
            minimumAmount: command.MinimumAmount,
            maximumAmount: command.MaximumAmount,
            settlementLeadDays: command.SettlementLeadDays,
            feePercent: command.FeePercent,
            flatFeeAmount: command.FlatFeeAmount,
            priority: command.Priority,
            effectiveFromDate: command.EffectiveFromDate,
            effectiveToDate: command.EffectiveToDate,
            notes: command.Notes);

        _dbContext.WalletRules.Add(rule);
        _dbContext.SaveChanges();
        return GetWalletRule(rule.Id)!;
    }

    public WalletRuleDetailView Submit(long walletRuleId, long actorUserId)
    {
        var rule = _dbContext.WalletRules.Single(entry => entry.Id == walletRuleId);
        AccountingPeriodLockGuard.EnsureOpenPeriod(_dbContext, rule.EffectiveFromDate, "wallet rule");
        rule.Submit(actorUserId);

        var workflowInstance = new WorkflowInstance(
            moduleCode: "FINANCE",
            documentType: "WALLET_RULE",
            documentId: rule.Id,
            makerUserId: actorUserId,
            workflowRuleId: 1L);
        workflowInstance.Submit(actorUserId, "Wallet rule submitted for checker approval.");

        _dbContext.WorkflowInstances.Add(workflowInstance);
        _dbContext.SaveChanges();
        return GetWalletRule(rule.Id)!;
    }

    public WalletRuleDetailView Approve(long walletRuleId, long actorUserId)
    {
        var rule = _dbContext.WalletRules.Single(entry => entry.Id == walletRuleId);
        AccountingPeriodLockGuard.EnsureOpenPeriod(_dbContext, rule.EffectiveFromDate, "wallet rule");
        rule.Approve(actorUserId);

        var workflowInstance = _dbContext.WorkflowInstances
            .Include(entry => entry.Actions)
            .SingleOrDefault(entry => entry.DocumentType == "WALLET_RULE" && entry.DocumentId == walletRuleId);
        workflowInstance?.Approve(actorUserId, "Wallet rule approved.");

        _dbContext.SaveChanges();
        return GetWalletRule(rule.Id)!;
    }

    public WalletRuleDetailView Activate(long walletRuleId, long actorUserId)
    {
        var rule = _dbContext.WalletRules.Single(entry => entry.Id == walletRuleId);
        AccountingPeriodLockGuard.EnsureOpenPeriod(_dbContext, rule.EffectiveFromDate, "wallet rule");
        rule.Activate(actorUserId);
        _dbContext.SaveChanges();
        return GetWalletRule(rule.Id)!;
    }

    public WalletRuleEvaluationView Evaluate(WalletRuleEvaluationCommand command)
    {
        if (command.RequestedAmount <= 0m)
        {
            throw new InvalidOperationException("Wallet evaluation requested amount must be greater than zero.");
        }

        var normalizedPartyTypeCode = WalletRuleCatalog.NormalizePartyTypeCode(command.PartyTypeCode);
        var normalizedTransactionTypeCode = WalletRuleCatalog.NormalizeTransactionTypeCode(command.TransactionTypeCode);
        var normalizedWalletSchemeCode = WalletRuleCatalog.NormalizeWalletSchemeCode(command.WalletSchemeCode);
        var normalizedCurrencyCode = WalletRuleCatalog.NormalizeCurrencyCode(command.CurrencyCode);

        var matchedRules = _dbContext.WalletRules
            .AsNoTracking()
            .ToArray()
            .Where(rule => rule.AppliesTo(
                command.BranchId,
                normalizedPartyTypeCode,
                normalizedTransactionTypeCode,
                normalizedWalletSchemeCode,
                normalizedCurrencyCode,
                command.RequestedAmount,
                command.EvaluationDate))
            .OrderByDescending(rule => rule.Priority)
            .ThenByDescending(rule => rule.GetSpecificityScore())
            .ThenByDescending(rule => rule.EffectiveFromDate)
            .ThenBy(rule => rule.Id)
            .ToArray();

        var lines = matchedRules.Select((rule, index) =>
        {
            var estimatedFeeAmount = Math.Round(
                (command.RequestedAmount * rule.FeePercent / 100m) + rule.FlatFeeAmount,
                2,
                MidpointRounding.AwayFromZero);
            var estimatedSettlementAmount = string.Equals(rule.TransactionTypeCode, "COLLECTION", StringComparison.OrdinalIgnoreCase)
                ? command.RequestedAmount - estimatedFeeAmount
                : command.RequestedAmount + estimatedFeeAmount;

            return new WalletRuleEvaluationLineView(
                rule.Id,
                rule.RuleCode,
                rule.RuleName,
                rule.BranchId,
                rule.PartyTypeCode,
                rule.TransactionTypeCode,
                rule.WalletSchemeCode,
                rule.IntegrationProviderCode,
                rule.CurrencyCode,
                rule.MinimumAmount,
                rule.MaximumAmount,
                command.RequestedAmount,
                estimatedFeeAmount,
                estimatedSettlementAmount,
                rule.SettlementLeadDays,
                rule.Priority,
                index == 0 ? "RECOMMENDED" : "ELIGIBLE",
                rule.Notes);
        }).ToArray();

        var recommendedRule = matchedRules.FirstOrDefault();
        return new WalletRuleEvaluationView(
            command.BranchId,
            normalizedPartyTypeCode,
            normalizedTransactionTypeCode,
            normalizedWalletSchemeCode,
            normalizedCurrencyCode,
            command.RequestedAmount,
            command.EvaluationDate,
            recommendedRule?.Id,
            recommendedRule?.RuleCode,
            recommendedRule?.IntegrationProviderCode,
            lines);
    }

    private static WalletRuleDetailView Map(WalletRule rule) =>
        new(
            rule.Id,
            rule.RuleCode,
            rule.RuleName,
            rule.BranchId,
            rule.PartyTypeCode,
            rule.TransactionTypeCode,
            rule.WalletSchemeCode,
            rule.IntegrationProviderCode,
            rule.CurrencyCode,
            rule.MinimumAmount,
            rule.MaximumAmount,
            rule.SettlementLeadDays,
            rule.FeePercent,
            rule.FlatFeeAmount,
            rule.Priority,
            rule.EffectiveFromDate,
            rule.EffectiveToDate,
            WorkflowStatusSeedIds.ToCode(rule.StatusId),
            rule.IsActive,
            rule.Notes,
            rule.SubmittedAtUtc,
            rule.SubmittedByUserId,
            rule.ApprovedAtUtc,
            rule.ApprovedByUserId,
            rule.ActivatedAtUtc,
            rule.ActivatedByUserId);
}
