using Microsoft.EntityFrameworkCore;
using ROLAC.API.Data;
using ROLAC.API.DTOs.Finance;
using ROLAC.API.Entities;

namespace ROLAC.API.Services;

/// <summary>
/// Read-only aggregation over Givings + Expenses for the Finance Dashboard.
/// Expense scope is Paid+Approved everywhere (decided with the user); income is Givings only.
/// </summary>
public class FinanceDashboardService : IFinanceDashboardService
{
    private readonly AppDbContext _db;
    public FinanceDashboardService(AppDbContext db) => _db = db;

    // Paid+Approved expenses, optionally bounded by ExpenseDate. Shared by the
    // income/expense pie and the breakdown so the filter is identical in both.
    private IQueryable<Expense> PaidApproved(DateOnly? from, DateOnly? to)
    {
        var q = _db.Expenses.Where(e => e.Status == "Paid" || e.Status == "Approved");
        if (from.HasValue) q = q.Where(e => e.ExpenseDate >= from.Value);
        if (to.HasValue)   q = q.Where(e => e.ExpenseDate <= to.Value);
        return q;
    }

    public async Task<FinanceSummaryDto> GetSummaryAsync()
    {
        var income  = await _db.Givings.SumAsync(g => (decimal?)g.Amount) ?? 0m;
        var expense = await PaidApproved(null, null).SumAsync(e => (decimal?)e.Amount) ?? 0m;
        return new FinanceSummaryDto
        {
            TotalIncome   = income,
            TotalExpenses = expense,
            Balance       = income - expense,
        };
    }

    public async Task<IncomeExpenseDto> GetIncomeExpenseAsync(DateOnly? from, DateOnly? to)
    {
        var giving = _db.Givings.AsQueryable();
        if (from.HasValue) giving = giving.Where(g => g.GivingDate >= from.Value);
        if (to.HasValue)   giving = giving.Where(g => g.GivingDate <= to.Value);

        return new IncomeExpenseDto
        {
            Income  = await giving.SumAsync(g => (decimal?)g.Amount) ?? 0m,
            Expense = await PaidApproved(from, to).SumAsync(e => (decimal?)e.Amount) ?? 0m,
        };
    }

    public async Task<List<BreakdownSliceDto>> GetExpenseBreakdownAsync(
        DateOnly? from, DateOnly? to, int? ministryId, int? categoryGroupId)
    {
        var q = PaidApproved(from, to);
        if (ministryId.HasValue) q = q.Where(e => e.MinistryId == ministryId.Value);

        // Lines belonging to the scoped (Paid+Approved, optionally ministry-filtered) expenses.
        var scopedLines = from l in _db.ExpenseLines
                          join e in q on l.ExpenseId equals e.Id
                          select l;

        // Group by the deepest level whose parent id is supplied. Category levels aggregate
        // over LINES (line amounts); the ministry level uses the header total to avoid
        // double-counting a multi-line expense across its lines.
        List<(int Id, decimal Amount)> grouped;
        if (categoryGroupId.HasValue)
            grouped = (await scopedLines.Where(l => l.CategoryGroupId == categoryGroupId.Value)
                .GroupBy(l => l.SubCategoryId)
                .Select(g => new { Id = g.Key, Amount = g.Sum(x => x.Amount) }).ToListAsync())
                .Select(x => (x.Id, x.Amount)).ToList();
        else if (ministryId.HasValue)
            grouped = (await scopedLines.GroupBy(l => l.CategoryGroupId)
                .Select(g => new { Id = g.Key, Amount = g.Sum(x => x.Amount) }).ToListAsync())
                .Select(x => (x.Id, x.Amount)).ToList();
        else
            grouped = (await q.GroupBy(e => e.MinistryId)
                .Select(g => new { Id = g.Key, Amount = g.Sum(x => x.Amount) }).ToListAsync())
                .Select(x => (x.Id, x.Amount)).ToList();

        var ids = grouped.Select(x => x.Id).ToHashSet();
        Dictionary<int, (string En, string? Zh)> names = categoryGroupId.HasValue
            ? await _db.ExpenseSubCategories.Where(s => ids.Contains(s.Id))
                  .ToDictionaryAsync(s => s.Id, s => (s.Name_en, s.Name_zh))
            : ministryId.HasValue
            ? await _db.ExpenseCategoryGroups.Where(g => ids.Contains(g.Id))
                  .ToDictionaryAsync(g => g.Id, g => (g.Name_en, g.Name_zh))
            : await _db.Ministries.Where(m => ids.Contains(m.Id))
                  .ToDictionaryAsync(m => m.Id, m => (m.Name_en, m.Name_zh));

        return grouped.Select(x =>
        {
            names.TryGetValue(x.Id, out var n);
            return new BreakdownSliceDto { Id = x.Id, Name_en = n.En ?? "", Name_zh = n.Zh, Amount = x.Amount };
        }).OrderByDescending(s => s.Amount).ToList();
    }
}
