using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using ROLAC.API.Data;
using ROLAC.API.DTOs.Expense;
using ROLAC.API.Entities;

namespace ROLAC.API.Services;

public class ExpenseSnapshotService : IExpenseSnapshotService
{
    private readonly AppDbContext _db;
    private readonly IHttpContextAccessor _http;
    public ExpenseSnapshotService(AppDbContext db, IHttpContextAccessor http)
    { _db = db; _http = http; }

    // The JWT carries the user id in the "sub" claim (NameClaimType="sub", MapInboundClaims=false),
    // so ClaimTypes.NameIdentifier is absent at runtime. Check NameIdentifier first (unit tests set it),
    // then fall back to "sub" (real tokens). Required for the self-ownership guard to work in production.
    private string CurrentUserId =>
        _http.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier)
        ?? _http.HttpContext?.User.FindFirstValue("sub")
        ?? "system";

    public async Task<List<ExpenseSnapshotDto>> GetAllAsync()
    {
        var snaps = await _db.ExpenseSnapshots.AsNoTracking()
            .OrderByDescending(s => s.CreatedAt).ThenByDescending(s => s.Id)
            .ToListAsync();
        if (snaps.Count == 0) return new();

        var ids = snaps.Select(s => s.Id).ToList();
        var lines = await _db.ExpenseSnapshotLines.AsNoTracking()
            .Where(l => ids.Contains(l.SnapshotId)).ToListAsync();
        var linesBySnapshot = lines.GroupBy(l => l.SnapshotId).ToDictionary(g => g.Key, g => g.ToList());

        var minNames = await _db.Ministries.AsNoTracking().ToDictionaryAsync(m => m.Id, m => $"{m.Name_en} / {m.Name_zh}");
        var creatorNames = await ResolveUserNamesAsync(snaps.Select(s => s.CreatedBy));

        return snaps.Select(s =>
        {
            linesBySnapshot.TryGetValue(s.Id, out var ls);
            return new ExpenseSnapshotDto
            {
                Id = s.Id, Name = s.Name, MinistryId = s.MinistryId,
                MinistryName = minNames.GetValueOrDefault(s.MinistryId, ""),
                Description = s.Description, VendorName = s.VendorName,
                CheckNumber = s.CheckNumber, Notes = s.Notes,
                TotalAmount = ls?.Sum(l => l.Amount) ?? 0,
                LineCount = ls?.Count ?? 0,
                CreatedByName = creatorNames.GetValueOrDefault(s.CreatedBy),
                CreatedAt = s.CreatedAt,
            };
        }).ToList();
    }

    public async Task<ExpenseSnapshotDto?> GetByIdAsync(int id)
    {
        var s = await _db.ExpenseSnapshots.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id);
        if (s is null) return null;

        var lines = await _db.ExpenseSnapshotLines.AsNoTracking()
            .Where(l => l.SnapshotId == id).OrderBy(l => l.Id).ToListAsync();
        var minName = await _db.Ministries.Where(m => m.Id == s.MinistryId).Select(m => m.Name_en).FirstOrDefaultAsync() ?? "";
        var grpNames = await _db.ExpenseCategoryGroups.AsNoTracking().ToDictionaryAsync(g => g.Id, g => g.Name_en);
        var subNames = await _db.ExpenseSubCategories.AsNoTracking().ToDictionaryAsync(x => x.Id, x => x.Name_en);
        var creatorName = (await ResolveUserNamesAsync(new[] { s.CreatedBy })).GetValueOrDefault(s.CreatedBy);

        return new ExpenseSnapshotDto
        {
            Id = s.Id, Name = s.Name, MinistryId = s.MinistryId, MinistryName = minName,
            Description = s.Description, VendorName = s.VendorName, CheckNumber = s.CheckNumber, Notes = s.Notes,
            TotalAmount = lines.Sum(l => l.Amount), LineCount = lines.Count,
            CreatedByName = creatorName, CreatedAt = s.CreatedAt,
            Lines = lines.Select(l => new ExpenseSnapshotLineDto
            {
                CategoryGroupId = l.CategoryGroupId, CategoryGroupName = grpNames.GetValueOrDefault(l.CategoryGroupId, ""),
                SubCategoryId = l.SubCategoryId, SubCategoryName = subNames.GetValueOrDefault(l.SubCategoryId, ""),
                FunctionalClass = l.FunctionalClass, Amount = l.Amount, Description = l.Description,
            }).ToList(),
        };
    }

    public async Task<int> CreateAsync(CreateExpenseSnapshotRequest r)
    {
        ValidateLines(r.Lines);
        var s = new ExpenseSnapshot
        {
            Name = r.Name.Trim(), MinistryId = r.MinistryId, Description = r.Description,
            VendorName = r.VendorName, CheckNumber = r.CheckNumber, Notes = r.Notes,
            Lines = BuildLines(r.Lines),
        };
        _db.ExpenseSnapshots.Add(s);
        await _db.SaveChangesAsync();
        return s.Id;
    }

    public async Task UpdateAsync(int id, UpdateExpenseSnapshotRequest r)
    {
        ValidateLines(r.Lines);
        var s = await _db.ExpenseSnapshots.Include(x => x.Lines).FirstOrDefaultAsync(x => x.Id == id)
            ?? throw new KeyNotFoundException($"Snapshot {id} not found.");

        s.Name = r.Name.Trim(); s.MinistryId = r.MinistryId; s.Description = r.Description;
        s.VendorName = r.VendorName; s.CheckNumber = r.CheckNumber; s.Notes = r.Notes;

        _db.ExpenseSnapshotLines.RemoveRange(s.Lines);
        s.Lines = BuildLines(r.Lines);
        await _db.SaveChangesAsync();
    }

    public async Task DeleteAsync(int id)
    {
        var s = await _db.ExpenseSnapshots.FirstOrDefaultAsync(x => x.Id == id)
            ?? throw new KeyNotFoundException($"Snapshot {id} not found.");
        s.IsDeleted = true; s.DeletedAt = DateTimeOffset.UtcNow; s.DeletedBy = CurrentUserId;
        await _db.SaveChangesAsync();
    }

    private static void ValidateLines(List<ExpenseLineInput> lines)
    {
        if (lines is null || lines.Count == 0)
            throw new InvalidOperationException("A snapshot must have at least one line.");
        foreach (var l in lines)
        {
            if (l.CategoryGroupId <= 0 || l.SubCategoryId <= 0)
                throw new InvalidOperationException("Each snapshot line needs a category group and subcategory.");
            if (l.Amount <= 0)
                throw new InvalidOperationException("Each snapshot line amount must be greater than zero.");
            if (l.FunctionalClass is not null && !FunctionalClasses.All.Contains(l.FunctionalClass))
                throw new InvalidOperationException($"Invalid functional class '{l.FunctionalClass}'.");
        }
    }

    private static List<ExpenseSnapshotLine> BuildLines(List<ExpenseLineInput> inputs) =>
        inputs.Select(l => new ExpenseSnapshotLine
        {
            CategoryGroupId = l.CategoryGroupId, SubCategoryId = l.SubCategoryId,
            FunctionalClass = l.FunctionalClass, Amount = l.Amount, Description = l.Description,
        }).ToList();

    // Resolve actor user ids (AppUser.Id, stored in CreatedBy) to a display name: the linked
    // Member's full name when present, otherwise the account email. Mirrors ExpenseService.
    private async Task<Dictionary<string, string>> ResolveUserNamesAsync(IEnumerable<string?> userIds)
    {
        var ids = userIds.Where(id => !string.IsNullOrEmpty(id)).Select(id => id!).Distinct().ToList();
        if (ids.Count == 0) return new();

        var users = await _db.Users.AsNoTracking()
            .Where(u => ids.Contains(u.Id))
            .Select(u => new { u.Id, u.Email, u.MemberId })
            .ToListAsync();

        var memberIds = users.Where(u => u.MemberId != null).Select(u => u.MemberId!.Value).ToHashSet();
        var memberNames = await _db.Members.AsNoTracking()
            .Where(m => memberIds.Contains(m.Id))
            .ToDictionaryAsync(m => m.Id, m => $"{m.FirstName_en} {m.LastName_en}".Trim());

        return users.ToDictionary(
            u => u.Id,
            u => u.MemberId != null && memberNames.TryGetValue(u.MemberId.Value, out var name) && name.Length > 0
                ? name
                : (u.Email ?? u.Id));
    }
}
