using MediatR;
using Microsoft.EntityFrameworkCore;
using SolutionErp.Application.Common.Interfaces;

namespace SolutionErp.Application.Office;

// Phase 11 P11-E (S?? 2026-06-08) — Báo cáo chấm công tháng + OT quy đổi theo OtPolicy.
// Aggregate Attendance theo tháng × phòng ban, phân loại OT thường/cuối tuần/lễ,
// nhân hệ số OtPolicy active → OT quy đổi (weighted). Admin-only report (controller guard).
//
// GOTCHA day-type (S?? em main spec): phân loại day-type dùng DayOfWeek + holidaySet
// KHÔNG EF-translate được → .ToListAsync() rồi classify IN-MEMORY (C#). KHÔNG để trong IQueryable.
// GOTCHA Holiday.Date = DateOnly (KHÔNG phải DateTime) → HashSet<DateOnly> + so qua
// DateOnly.FromDateTime(a.AttendanceDate) (spec viết HashSet<DateTime> nhầm type — dùng DateOnly đúng).

public record AttendanceReportRowDto(Guid UserId, string FullName, string? DepartmentName, int DaysPresent,
    decimal TotalWorkHours, decimal OtRaw, decimal OtWeekday, decimal OtWeekend, decimal OtHoliday, decimal OtWeighted);

public record AttendanceReportDto(int Year, int Month, IReadOnlyList<AttendanceReportRowDto> Rows,
    decimal GrandTotalWorkHours, decimal GrandTotalOtWeighted);

public record GetAttendanceReportQuery(int Year, int Month, Guid? DepartmentId) : IRequest<AttendanceReportDto>;

public class GetAttendanceReportHandler(IApplicationDbContext db)
    : IRequestHandler<GetAttendanceReportQuery, AttendanceReportDto>
{
    public async Task<AttendanceReportDto> Handle(GetAttendanceReportQuery q, CancellationToken ct)
    {
        // 1. OtPolicy active → hệ số nhân (fallback 1.5 / 2.0 / 3.0 nếu chưa cấu hình).
        var policy = await db.OtPolicies.AsNoTracking()
            .FirstOrDefaultAsync(p => p.IsActive && !p.IsDeleted, ct);
        var mWd = policy?.MultiplierWeekday ?? 1.5m;
        var mWe = policy?.MultiplierWeekend ?? 2.0m;
        var mHol = policy?.MultiplierHoliday ?? 3.0m;

        // 2. Holiday set của năm → HashSet<DateOnly> (Holiday.Date = DateOnly).
        var holidayList = await db.Holidays.AsNoTracking()
            .Where(h => h.Year == q.Year && !h.IsDeleted)
            .Select(h => h.Date)
            .ToListAsync(ct);
        var holidaySet = holidayList.ToHashSet();

        // 3. Attendance tháng (+ lọc phòng ban qua join Users nếu có DepartmentId).
        var attQuery = db.Attendances.AsNoTracking()
            .Where(a => !a.IsDeleted && a.AttendanceDate.Year == q.Year && a.AttendanceDate.Month == q.Month);
        if (q.DepartmentId is Guid deptId)
        {
            attQuery = from a in attQuery
                       join u in db.Users.AsNoTracking() on a.UserId equals u.Id
                       where u.DepartmentId == deptId
                       select a;
        }

        // ⚠️ Materialize TRƯỚC khi classify day-type (DayOfWeek + holidaySet không EF-translate).
        var rows = await attQuery.ToListAsync(ct);

        // 4. Denorm FullName + DepartmentName (FullName ưu tiên Attendance.UserFullName đã denorm).
        var userIds = rows.Select(r => r.UserId).Distinct().ToList();
        var userMeta = await (from u in db.Users.AsNoTracking()
                              where userIds.Contains(u.Id)
                              join d in db.Departments.AsNoTracking() on u.DepartmentId equals d.Id into dj
                              from d in dj.DefaultIfEmpty()
                              select new { u.Id, u.FullName, DepartmentName = d != null ? d.Name : null })
            .ToListAsync(ct);
        var metaByUser = userMeta.ToDictionary(x => x.Id);

        // 5. Group by UserId + classify IN-MEMORY.
        var reportRows = rows
            .GroupBy(a => a.UserId)
            .Select(g =>
            {
                var meta = metaByUser.GetValueOrDefault(g.Key);
                var fullName = g.Select(a => a.UserFullName).FirstOrDefault(n => !string.IsNullOrWhiteSpace(n))
                               ?? meta?.FullName ?? "(unknown)";

                var daysPresent = g.Count(a => a.CheckInAt != null);
                var totalWork = g.Sum(a => a.WorkHours ?? 0m);
                var otRaw = g.Sum(a => a.OtHours ?? 0m);

                decimal otWeekday = 0m, otWeekend = 0m, otHoliday = 0m;
                foreach (var a in g)
                {
                    var ot = a.OtHours ?? 0m;
                    if (ot == 0m) continue;
                    var d = a.AttendanceDate.Date;
                    if (holidaySet.Contains(DateOnly.FromDateTime(d)))
                        otHoliday += ot;
                    else if (d.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday)
                        otWeekend += ot;
                    else
                        otWeekday += ot;
                }

                var otWeighted = otWeekday * mWd + otWeekend * mWe + otHoliday * mHol;

                return new AttendanceReportRowDto(g.Key, fullName, meta?.DepartmentName, daysPresent,
                    totalWork, otRaw, otWeekday, otWeekend, otHoliday, otWeighted);
            })
            .OrderBy(r => r.FullName, StringComparer.CurrentCulture)
            .ToList();

        var grandWork = reportRows.Sum(r => r.TotalWorkHours);
        var grandOtWeighted = reportRows.Sum(r => r.OtWeighted);

        return new AttendanceReportDto(q.Year, q.Month, reportRows, grandWork, grandOtWeighted);
    }
}
