namespace BudgetApp.Application.Services;

using BudgetApp.Domain.Interfaces;
using BudgetApp.Domain.Models;

/// <summary>
/// Application service for ledger operations.
/// Handles recording expenses and income with budget allocations.
/// </summary>
public class LedgerService
{
    private readonly ILedgerRepository _ledgerRepository;
    private readonly IBudgetRepository _budgetRepository;

    public LedgerService(ILedgerRepository ledgerRepository, IBudgetRepository budgetRepository)
    {
        _ledgerRepository = ledgerRepository ?? throw new ArgumentNullException(nameof(ledgerRepository));
        _budgetRepository = budgetRepository ?? throw new ArgumentNullException(nameof(budgetRepository));
    }

    /// <summary>
    /// Records an expense with budget allocations.
    /// Validates that allocations sum to expense amount and updates budget balances.
    /// </summary>
    public async Task<LedgerEntry> RecordExpenseAsync(
        DateTime date,
        string description,
        Money amount,
        Dictionary<Guid, Money> budgetAllocations)
    {
        if (string.IsNullOrWhiteSpace(description))
            throw new ArgumentException("Description cannot be null or empty.", nameof(description));
        
        if (amount == null)
            throw new ArgumentNullException(nameof(amount));
        
        if (budgetAllocations == null || budgetAllocations.Count == 0)
            throw new ArgumentException("Budget allocations cannot be null or empty.", nameof(budgetAllocations));

        // Validate that all budgets exist
        await ValidateBudgetsExistAsync(budgetAllocations.Keys);

        // Validate that allocations sum to expense amount (cross-model validation)
        ValidateBudgetAllocationsSum(amount, budgetAllocations);

        // Create and validate ledger entry
        var entry = new LedgerEntry(date, description, amount, EntryType.Expense, budgetAllocations);
        entry.Validate(); // Domain validation

        // Update budget balances
        await UpdateBudgetBalancesForExpenseAsync(budgetAllocations);

        // Save entry to ledger
        var ledger = await _ledgerRepository.GetOrCreateAsync();
        ledger.AddEntry(entry);
        await _ledgerRepository.SaveAsync(ledger);
        await _ledgerRepository.SaveEntryAsync(entry);

        return entry;
    }

    /// <summary>
    /// Records income with budget allocations.
    /// Validates that allocations sum to income amount and updates budget balances.
    /// </summary>
    public async Task<LedgerEntry> RecordIncomeAsync(
        DateTime date,
        string description,
        Money amount,
        Dictionary<Guid, Money> budgetAllocations)
    {
        if (string.IsNullOrWhiteSpace(description))
            throw new ArgumentException("Description cannot be null or empty.", nameof(description));
        
        if (amount == null)
            throw new ArgumentNullException(nameof(amount));
        
        if (budgetAllocations == null || budgetAllocations.Count == 0)
            throw new ArgumentException("Budget allocations cannot be null or empty.", nameof(budgetAllocations));

        // Validate that all budgets exist
        await ValidateBudgetsExistAsync(budgetAllocations.Keys);

        // Validate that allocations sum to income amount (cross-model validation)
        ValidateBudgetAllocationsSum(amount, budgetAllocations);

        // Create and validate ledger entry
        var entry = new LedgerEntry(date, description, amount, EntryType.Income, budgetAllocations);
        entry.Validate(); // Domain validation

        // Update budget balances
        await UpdateBudgetBalancesForIncomeAsync(budgetAllocations);

        // Save entry to ledger
        var ledger = await _ledgerRepository.GetOrCreateAsync();
        ledger.AddEntry(entry);
        await _ledgerRepository.SaveAsync(ledger);
        await _ledgerRepository.SaveEntryAsync(entry);

        return entry;
    }

    /// <summary>
    /// Gets all ledger entries.
    /// </summary>
    public async Task<IEnumerable<LedgerEntry>> GetLedgerEntriesAsync()
    {
        return await _ledgerRepository.GetAllEntriesAsync();
    }

    /// <summary>
    /// Gets the ledger.
    /// </summary>
    public async Task<Ledger> GetLedgerAsync()
    {
        return await _ledgerRepository.GetOrCreateAsync();
    }

    /// <summary>
    /// Gets the total balance across all budgets for a specific currency.
    /// </summary>
    public async Task<Money> GetTotalBudgetBalanceAsync(string currency)
    {
        var budgets = await _budgetRepository.GetAllAsync();
        var total = budgets
            .Where(b => b.Balance.Currency == currency)
            .Sum(b => b.Balance.Amount);

        return new Money(total, currency);
    }

    /// <summary>
    /// Validates that all budget IDs exist.
    /// </summary>
    private async Task ValidateBudgetsExistAsync(IEnumerable<Guid> budgetIds)
    {
        var allBudgets = await _budgetRepository.GetAllAsync();
        var existingIds = allBudgets.Select(b => b.Id).ToHashSet();

        var missingIds = budgetIds.Where(id => !existingIds.Contains(id)).ToList();
        if (missingIds.Any())
        {
            throw new InvalidOperationException(
                $"The following budget IDs do not exist: {string.Join(", ", missingIds)}");
        }
    }

    /// <summary>
    /// Validates that budget allocations sum to the entry amount (cross-model validation).
    /// </summary>
    private void ValidateBudgetAllocationsSum(Money entryAmount, Dictionary<Guid, Money> budgetAllocations)
    {
        var totalAllocated = budgetAllocations.Values
            .Aggregate(new Money(0, entryAmount.Currency), (sum, money) => sum.Add(money));

        if (totalAllocated.Amount != entryAmount.Amount)
        {
            throw new InvalidOperationException(
                $"Entry amount ({entryAmount.Amount}) does not match sum of budget allocations ({totalAllocated.Amount}).");
        }
    }

    /// <summary>
    /// Updates budget balances for an expense (removes money from budgets).
    /// </summary>
    private async Task UpdateBudgetBalancesForExpenseAsync(Dictionary<Guid, Money> budgetAllocations)
    {
        foreach (var allocation in budgetAllocations)
        {
            var budget = await _budgetRepository.GetByIdAsync(allocation.Key);
            if (budget == null)
                throw new InvalidOperationException($"Budget with ID {allocation.Key} not found.");

            budget.RemoveMoney(allocation.Value);
            await _budgetRepository.SaveAsync(budget);
        }
    }

    /// <summary>
    /// Updates budget balances for income (adds money to budgets).
    /// </summary>
    private async Task UpdateBudgetBalancesForIncomeAsync(Dictionary<Guid, Money> budgetAllocations)
    {
        foreach (var allocation in budgetAllocations)
        {
            var budget = await _budgetRepository.GetByIdAsync(allocation.Key);
            if (budget == null)
                throw new InvalidOperationException($"Budget with ID {allocation.Key} not found.");

            budget.AddMoney(allocation.Value);
            await _budgetRepository.SaveAsync(budget);
        }
    }
}

