using FluentValidation;
using MediatR;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using SolutionErp.Application.Common.Exceptions;
using SolutionErp.Application.Common.Interfaces;
using SolutionErp.Application.Common.Models;
using SolutionErp.Application.Hrm.Dtos;
using SolutionErp.Application.Hrm.Services;
using SolutionErp.Domain.Hrm;
using SolutionErp.Domain.Identity;

namespace SolutionErp.Application.Hrm;

// Phase 10.1 G-H1 Task 4 (S33) — Hồ sơ Nhân sự CQRS bundle.
// 5 main endpoint Phase 1 minimal: Create / Update / Delete (soft) / Get / List.
// Satellite CRUD endpoint (WorkHistory/Education/FamilyRelation/Skill/Document)
// DEFER Phase 1.5 — GetDetail trả về 5 satellite collection inline cho FE Tab
// hiển thị nháy mắt. Bro UAT có thể request mở satellite write Phase 1.5 sau.
//
// Pattern mirror PurchaseEvaluationFeatures.cs (Pattern 12-bis cross-module
// cookie-cutter PE → Hrm — 4th reinforcement post Plan B Chunk C S29).

// ========== CREATE ==========

public record CreateEmployeeProfileCommand(
    Guid UserId,
    EmployeeStatus EmployeeStatus = EmployeeStatus.Active,
    Gender? Gender = null,
    MaritalStatus? MaritalStatus = null,
    EmployeeType? EmployeeType = null,
    DateOnly? DateOfBirth = null,
    string? BirthPlace = null,
    string? Hometown = null,
    string? Phone = null,
    string? PersonalEmail = null,
    string? InternalPhone = null,
    string? Ethnicity = null,
    string? Religion = null,
    string? Nationality = null,
    string? IdCardNumber = null,
    DateOnly? IdCardIssueDate = null,
    string? IdCardIssuePlace = null,
    string? TaxCode = null,
    string? SocialInsuranceNumber = null,
    string? PassportNumber = null,
    string? PermanentAddressText = null,
    string? StreetAddressPermanent = null,
    string? TemporaryAddressText = null,
    string? StreetAddressTemporary = null,
    DateOnly? HireDate = null,
    string? EmergencyContactName = null,
    string? EmergencyContactPhone = null,
    string? EmergencyContactAddress = null,
    string? Qualification = null,
    string? AcademicTitle = null,
    string? WorkLocation = null,
    string? TimekeepingCode = null,
    string? BankAccount = null,
    string? BankName = null,
    string? BankBranch = null,
    int? HeightCm = null,
    int? WeightKg = null,
    string? BloodType = null,
    decimal? BaseSalary = null,
    decimal? TotalSalary = null,
    decimal? AnnualLeaveDays = null,
    DateOnly? SocialInsuranceStartDate = null,
    string? MedicalRegistrationPlace = null,
    bool IsCommunistParty = false,
    DateOnly? CommunistPartyJoinDate = null,
    bool IsYouthUnion = false,
    DateOnly? YouthUnionJoinDate = null,
    bool IsTradeUnion = false,
    DateOnly? TradeUnionJoinDate = null,
    string? PhotoUrl = null,
    string? Notes = null) : IRequest<Guid>;

public class CreateEmployeeProfileCommandValidator : AbstractValidator<CreateEmployeeProfileCommand>
{
    public CreateEmployeeProfileCommandValidator()
    {
        RuleFor(x => x.UserId).NotEmpty();
        RuleFor(x => x.EmployeeStatus).IsInEnum();

        // Liên hệ
        RuleFor(x => x.Phone).MaximumLength(20);
        RuleFor(x => x.PersonalEmail).MaximumLength(200).EmailAddress()
            .When(x => !string.IsNullOrWhiteSpace(x.PersonalEmail));
        RuleFor(x => x.InternalPhone).MaximumLength(20);

        // Cá nhân
        RuleFor(x => x.BirthPlace).MaximumLength(200);
        RuleFor(x => x.Hometown).MaximumLength(200);
        RuleFor(x => x.Ethnicity).MaximumLength(50);
        RuleFor(x => x.Religion).MaximumLength(50);
        RuleFor(x => x.Nationality).MaximumLength(50);

        // Giấy tờ
        RuleFor(x => x.IdCardNumber).MaximumLength(20);
        RuleFor(x => x.IdCardIssuePlace).MaximumLength(200);
        RuleFor(x => x.TaxCode).MaximumLength(20);
        RuleFor(x => x.SocialInsuranceNumber).MaximumLength(20);
        RuleFor(x => x.PassportNumber).MaximumLength(20);

        // Địa chỉ
        RuleFor(x => x.PermanentAddressText).MaximumLength(500);
        RuleFor(x => x.StreetAddressPermanent).MaximumLength(200);
        RuleFor(x => x.TemporaryAddressText).MaximumLength(500);
        RuleFor(x => x.StreetAddressTemporary).MaximumLength(200);

        // Khẩn cấp
        RuleFor(x => x.EmergencyContactName).MaximumLength(200);
        RuleFor(x => x.EmergencyContactPhone).MaximumLength(20);
        RuleFor(x => x.EmergencyContactAddress).MaximumLength(500);

        // Trình độ + vị trí
        RuleFor(x => x.Qualification).MaximumLength(200);
        RuleFor(x => x.AcademicTitle).MaximumLength(200);
        RuleFor(x => x.WorkLocation).MaximumLength(200);
        RuleFor(x => x.TimekeepingCode).MaximumLength(50);

        // Ngân hàng
        RuleFor(x => x.BankAccount).MaximumLength(50);
        RuleFor(x => x.BankName).MaximumLength(200);
        RuleFor(x => x.BankBranch).MaximumLength(200);

        // Sức khoẻ
        RuleFor(x => x.BloodType).MaximumLength(5);

        // Lương + Phép (decimal — không âm)
        RuleFor(x => x.BaseSalary).GreaterThanOrEqualTo(0).When(x => x.BaseSalary.HasValue);
        RuleFor(x => x.TotalSalary).GreaterThanOrEqualTo(0).When(x => x.TotalSalary.HasValue);
        RuleFor(x => x.AnnualLeaveDays).GreaterThanOrEqualTo(0).When(x => x.AnnualLeaveDays.HasValue);

        // BHXH
        RuleFor(x => x.MedicalRegistrationPlace).MaximumLength(200);

        // Khác
        RuleFor(x => x.PhotoUrl).MaximumLength(500);
        RuleFor(x => x.Notes).MaximumLength(2000);
    }
}

public class CreateEmployeeProfileCommandHandler(
    IApplicationDbContext db,
    IEmployeeCodeGenerator codeGen,
    UserManager<User> userManager) : IRequestHandler<CreateEmployeeProfileCommand, Guid>
{
    public async Task<Guid> Handle(CreateEmployeeProfileCommand request, CancellationToken ct)
    {
        // Verify User tồn tại (1-1 link User.Id).
        var user = await userManager.FindByIdAsync(request.UserId.ToString())
            ?? throw new NotFoundException("User", request.UserId);

        // Verify NO existing EmployeeProfile cho UserId này (UNIQUE 1-1 ràng buộc Mig 34).
        // Check cả soft-deleted để admin biết user đã từng có profile (re-activate Phase 1.5).
        var existing = await db.EmployeeProfiles.AsNoTracking()
            .FirstOrDefaultAsync(x => x.UserId == request.UserId, ct);
        if (existing != null)
            throw new ConflictException(existing.IsDeleted
                ? $"User {user.UserName} đã có hồ sơ NV (đã xoá mềm). Cần khôi phục thay vì tạo mới."
                : $"User {user.UserName} đã có hồ sơ NV — mỗi user chỉ được 1 hồ sơ.");

        // Atomic MaNhanVien sequence — format NV/{YYYY}/{Seq:D4} (mirror PE pattern).
        var employeeCode = await codeGen.GenerateAsync(ct);

        var entity = new EmployeeProfile
        {
            UserId = request.UserId,
            EmployeeCode = employeeCode,
            EmployeeStatus = request.EmployeeStatus,
            Gender = request.Gender,
            MaritalStatus = request.MaritalStatus,
            EmployeeType = request.EmployeeType,
            DateOfBirth = request.DateOfBirth,
            BirthPlace = request.BirthPlace,
            Hometown = request.Hometown,
            Phone = request.Phone,
            PersonalEmail = request.PersonalEmail,
            InternalPhone = request.InternalPhone,
            Ethnicity = request.Ethnicity,
            Religion = request.Religion,
            Nationality = request.Nationality ?? "Việt Nam",
            IdCardNumber = request.IdCardNumber,
            IdCardIssueDate = request.IdCardIssueDate,
            IdCardIssuePlace = request.IdCardIssuePlace,
            TaxCode = request.TaxCode,
            SocialInsuranceNumber = request.SocialInsuranceNumber,
            PassportNumber = request.PassportNumber,
            PermanentAddressText = request.PermanentAddressText,
            StreetAddressPermanent = request.StreetAddressPermanent,
            TemporaryAddressText = request.TemporaryAddressText,
            StreetAddressTemporary = request.StreetAddressTemporary,
            HireDate = request.HireDate,
            EmergencyContactName = request.EmergencyContactName,
            EmergencyContactPhone = request.EmergencyContactPhone,
            EmergencyContactAddress = request.EmergencyContactAddress,
            Qualification = request.Qualification,
            AcademicTitle = request.AcademicTitle,
            WorkLocation = request.WorkLocation,
            TimekeepingCode = request.TimekeepingCode,
            BankAccount = request.BankAccount,
            BankName = request.BankName,
            BankBranch = request.BankBranch,
            HeightCm = request.HeightCm,
            WeightKg = request.WeightKg,
            BloodType = request.BloodType,
            BaseSalary = request.BaseSalary,
            TotalSalary = request.TotalSalary,
            AnnualLeaveDays = request.AnnualLeaveDays,
            SocialInsuranceStartDate = request.SocialInsuranceStartDate,
            MedicalRegistrationPlace = request.MedicalRegistrationPlace,
            IsCommunistParty = request.IsCommunistParty,
            CommunistPartyJoinDate = request.CommunistPartyJoinDate,
            IsYouthUnion = request.IsYouthUnion,
            YouthUnionJoinDate = request.YouthUnionJoinDate,
            IsTradeUnion = request.IsTradeUnion,
            TradeUnionJoinDate = request.TradeUnionJoinDate,
            PhotoUrl = request.PhotoUrl,
            Notes = request.Notes,
        };

        db.EmployeeProfiles.Add(entity);
        await db.SaveChangesAsync(ct);
        return entity.Id;
    }
}

// ========== UPDATE ==========

// UserId + EmployeeCode immutable — admin KHÔNG đổi link user lẫn mã NV.
// Field mutable: toàn bộ phần còn lại Create command + ResignDate + 3 leave fields
// extra (RemainingLeaveDays/CompensatoryLeaveDays/SeniorityLeaveDays) cho admin chỉnh.
public record UpdateEmployeeProfileCommand(
    Guid Id,
    EmployeeStatus EmployeeStatus,
    Gender? Gender,
    MaritalStatus? MaritalStatus,
    EmployeeType? EmployeeType,
    DateOnly? DateOfBirth,
    string? BirthPlace,
    string? Hometown,
    string? Phone,
    string? PersonalEmail,
    string? InternalPhone,
    string? Ethnicity,
    string? Religion,
    string? Nationality,
    string? IdCardNumber,
    DateOnly? IdCardIssueDate,
    string? IdCardIssuePlace,
    string? TaxCode,
    string? SocialInsuranceNumber,
    string? PassportNumber,
    string? PermanentAddressText,
    string? StreetAddressPermanent,
    string? TemporaryAddressText,
    string? StreetAddressTemporary,
    DateOnly? HireDate,
    DateOnly? ResignDate,
    string? EmergencyContactName,
    string? EmergencyContactPhone,
    string? EmergencyContactAddress,
    string? Qualification,
    string? AcademicTitle,
    string? WorkLocation,
    string? TimekeepingCode,
    string? BankAccount,
    string? BankName,
    string? BankBranch,
    int? HeightCm,
    int? WeightKg,
    string? BloodType,
    decimal? BaseSalary,
    decimal? TotalSalary,
    decimal? AnnualLeaveDays,
    decimal? RemainingLeaveDays,
    decimal? CompensatoryLeaveDays,
    decimal? SeniorityLeaveDays,
    DateOnly? SocialInsuranceStartDate,
    string? MedicalRegistrationPlace,
    // Phase 1.5 S34 — bool → bool? safe partial update. FE chỉ gửi field admin
    // muốn đổi → null = KHÔNG đổi (giữ giá trị cũ). Reviewer minor #(b) S33.
    bool? IsCommunistParty,
    DateOnly? CommunistPartyJoinDate,
    bool? IsYouthUnion,
    DateOnly? YouthUnionJoinDate,
    bool? IsTradeUnion,
    DateOnly? TradeUnionJoinDate,
    string? PhotoUrl,
    string? Notes) : IRequest;

public class UpdateEmployeeProfileCommandValidator : AbstractValidator<UpdateEmployeeProfileCommand>
{
    public UpdateEmployeeProfileCommandValidator()
    {
        RuleFor(x => x.Id).NotEmpty();
        RuleFor(x => x.EmployeeStatus).IsInEnum();

        RuleFor(x => x.Phone).MaximumLength(20);
        RuleFor(x => x.PersonalEmail).MaximumLength(200).EmailAddress()
            .When(x => !string.IsNullOrWhiteSpace(x.PersonalEmail));
        RuleFor(x => x.InternalPhone).MaximumLength(20);

        RuleFor(x => x.BirthPlace).MaximumLength(200);
        RuleFor(x => x.Hometown).MaximumLength(200);
        RuleFor(x => x.Ethnicity).MaximumLength(50);
        RuleFor(x => x.Religion).MaximumLength(50);
        RuleFor(x => x.Nationality).MaximumLength(50);

        RuleFor(x => x.IdCardNumber).MaximumLength(20);
        RuleFor(x => x.IdCardIssuePlace).MaximumLength(200);
        RuleFor(x => x.TaxCode).MaximumLength(20);
        RuleFor(x => x.SocialInsuranceNumber).MaximumLength(20);
        RuleFor(x => x.PassportNumber).MaximumLength(20);

        RuleFor(x => x.PermanentAddressText).MaximumLength(500);
        RuleFor(x => x.StreetAddressPermanent).MaximumLength(200);
        RuleFor(x => x.TemporaryAddressText).MaximumLength(500);
        RuleFor(x => x.StreetAddressTemporary).MaximumLength(200);

        RuleFor(x => x.EmergencyContactName).MaximumLength(200);
        RuleFor(x => x.EmergencyContactPhone).MaximumLength(20);
        RuleFor(x => x.EmergencyContactAddress).MaximumLength(500);

        RuleFor(x => x.Qualification).MaximumLength(200);
        RuleFor(x => x.AcademicTitle).MaximumLength(200);
        RuleFor(x => x.WorkLocation).MaximumLength(200);
        RuleFor(x => x.TimekeepingCode).MaximumLength(50);

        RuleFor(x => x.BankAccount).MaximumLength(50);
        RuleFor(x => x.BankName).MaximumLength(200);
        RuleFor(x => x.BankBranch).MaximumLength(200);

        RuleFor(x => x.BloodType).MaximumLength(5);

        RuleFor(x => x.BaseSalary).GreaterThanOrEqualTo(0).When(x => x.BaseSalary.HasValue);
        RuleFor(x => x.TotalSalary).GreaterThanOrEqualTo(0).When(x => x.TotalSalary.HasValue);
        RuleFor(x => x.AnnualLeaveDays).GreaterThanOrEqualTo(0).When(x => x.AnnualLeaveDays.HasValue);
        RuleFor(x => x.RemainingLeaveDays).GreaterThanOrEqualTo(0).When(x => x.RemainingLeaveDays.HasValue);
        RuleFor(x => x.CompensatoryLeaveDays).GreaterThanOrEqualTo(0).When(x => x.CompensatoryLeaveDays.HasValue);
        RuleFor(x => x.SeniorityLeaveDays).GreaterThanOrEqualTo(0).When(x => x.SeniorityLeaveDays.HasValue);

        RuleFor(x => x.MedicalRegistrationPlace).MaximumLength(200);

        RuleFor(x => x.PhotoUrl).MaximumLength(500);
        RuleFor(x => x.Notes).MaximumLength(2000);
    }
}

public class UpdateEmployeeProfileCommandHandler(
    IApplicationDbContext db) : IRequestHandler<UpdateEmployeeProfileCommand>
{
    public async Task Handle(UpdateEmployeeProfileCommand request, CancellationToken ct)
    {
        var entity = await db.EmployeeProfiles
            .FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, ct)
            ?? throw new NotFoundException("EmployeeProfile", request.Id);

        // UserId + EmployeeCode immutable — bỏ qua mọi attempt update.
        entity.EmployeeStatus = request.EmployeeStatus;
        entity.Gender = request.Gender;
        entity.MaritalStatus = request.MaritalStatus;
        entity.EmployeeType = request.EmployeeType;
        entity.DateOfBirth = request.DateOfBirth;
        entity.BirthPlace = request.BirthPlace;
        entity.Hometown = request.Hometown;
        entity.Phone = request.Phone;
        entity.PersonalEmail = request.PersonalEmail;
        entity.InternalPhone = request.InternalPhone;
        entity.Ethnicity = request.Ethnicity;
        entity.Religion = request.Religion;
        entity.Nationality = request.Nationality ?? "Việt Nam";
        entity.IdCardNumber = request.IdCardNumber;
        entity.IdCardIssueDate = request.IdCardIssueDate;
        entity.IdCardIssuePlace = request.IdCardIssuePlace;
        entity.TaxCode = request.TaxCode;
        entity.SocialInsuranceNumber = request.SocialInsuranceNumber;
        entity.PassportNumber = request.PassportNumber;
        entity.PermanentAddressText = request.PermanentAddressText;
        entity.StreetAddressPermanent = request.StreetAddressPermanent;
        entity.TemporaryAddressText = request.TemporaryAddressText;
        entity.StreetAddressTemporary = request.StreetAddressTemporary;
        entity.HireDate = request.HireDate;
        entity.ResignDate = request.ResignDate;
        entity.EmergencyContactName = request.EmergencyContactName;
        entity.EmergencyContactPhone = request.EmergencyContactPhone;
        entity.EmergencyContactAddress = request.EmergencyContactAddress;
        entity.Qualification = request.Qualification;
        entity.AcademicTitle = request.AcademicTitle;
        entity.WorkLocation = request.WorkLocation;
        entity.TimekeepingCode = request.TimekeepingCode;
        entity.BankAccount = request.BankAccount;
        entity.BankName = request.BankName;
        entity.BankBranch = request.BankBranch;
        entity.HeightCm = request.HeightCm;
        entity.WeightKg = request.WeightKg;
        entity.BloodType = request.BloodType;
        entity.BaseSalary = request.BaseSalary;
        entity.TotalSalary = request.TotalSalary;
        entity.AnnualLeaveDays = request.AnnualLeaveDays;
        entity.RemainingLeaveDays = request.RemainingLeaveDays;
        entity.CompensatoryLeaveDays = request.CompensatoryLeaveDays;
        entity.SeniorityLeaveDays = request.SeniorityLeaveDays;
        entity.SocialInsuranceStartDate = request.SocialInsuranceStartDate;
        entity.MedicalRegistrationPlace = request.MedicalRegistrationPlace;
        // Phase 1.5 S34 — bool? partial update: null = KHÔNG đổi (giữ giá trị cũ).
        // FE chỉ gửi field admin muốn đổi tránh accidental reset (Reviewer minor S33).
        if (request.IsCommunistParty.HasValue) entity.IsCommunistParty = request.IsCommunistParty.Value;
        entity.CommunistPartyJoinDate = request.CommunistPartyJoinDate;
        if (request.IsYouthUnion.HasValue) entity.IsYouthUnion = request.IsYouthUnion.Value;
        entity.YouthUnionJoinDate = request.YouthUnionJoinDate;
        if (request.IsTradeUnion.HasValue) entity.IsTradeUnion = request.IsTradeUnion.Value;
        entity.TradeUnionJoinDate = request.TradeUnionJoinDate;
        entity.PhotoUrl = request.PhotoUrl;
        entity.Notes = request.Notes;

        await db.SaveChangesAsync(ct);
    }
}

// ========== DELETE (soft) ==========

public record DeleteEmployeeProfileCommand(Guid Id) : IRequest;

public class DeleteEmployeeProfileCommandHandler(
    IApplicationDbContext db,
    ICurrentUser currentUser) : IRequestHandler<DeleteEmployeeProfileCommand>
{
    public async Task Handle(DeleteEmployeeProfileCommand request, CancellationToken ct)
    {
        var entity = await db.EmployeeProfiles
            .FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, ct)
            ?? throw new NotFoundException("EmployeeProfile", request.Id);

        entity.IsDeleted = true;
        entity.DeletedAt = DateTime.UtcNow;
        entity.DeletedBy = currentUser.UserId;

        await db.SaveChangesAsync(ct);
    }
}

// ========== GET DETAIL ==========

public record GetEmployeeProfileQuery(Guid Id) : IRequest<EmployeeProfileDetailDto>;

public class GetEmployeeProfileQueryHandler(
    IApplicationDbContext db) : IRequestHandler<GetEmployeeProfileQuery, EmployeeProfileDetailDto>
{
    public async Task<EmployeeProfileDetailDto> Handle(GetEmployeeProfileQuery request, CancellationToken ct)
    {
        var entity = await db.EmployeeProfiles.AsNoTracking()
            .Include(x => x.WorkHistories)
            .Include(x => x.Educations)
            .Include(x => x.FamilyRelations)
            .Include(x => x.Skills)
            .Include(x => x.Documents)
            .FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, ct)
            ?? throw new NotFoundException("EmployeeProfile", request.Id);

        // JOIN User + Department LEFT (Department nullable per User entity).
        var userInfo = await (
            from u in db.Users.AsNoTracking()
            where u.Id == entity.UserId
            join d in db.Departments.AsNoTracking() on u.DepartmentId equals d.Id into dj
            from d in dj.DefaultIfEmpty()
            select new { u.FullName, u.Email, u.DepartmentId, DepartmentName = d != null ? d.Name : null })
            .FirstOrDefaultAsync(ct);

        return new EmployeeProfileDetailDto(
            entity.Id,
            entity.EmployeeCode,
            entity.UserId,
            userInfo?.FullName,
            userInfo?.Email,
            userInfo?.DepartmentId,
            userInfo?.DepartmentName,
            entity.EmployeeStatus,
            entity.Gender,
            entity.MaritalStatus,
            entity.EmployeeType,
            entity.DateOfBirth,
            entity.BirthPlace,
            entity.Hometown,
            entity.Phone,
            entity.PersonalEmail,
            entity.InternalPhone,
            entity.Ethnicity,
            entity.Religion,
            entity.Nationality,
            entity.IdCardNumber,
            entity.IdCardIssueDate,
            entity.IdCardIssuePlace,
            entity.TaxCode,
            entity.SocialInsuranceNumber,
            entity.PassportNumber,
            entity.PermanentAddressText,
            entity.StreetAddressPermanent,
            entity.TemporaryAddressText,
            entity.StreetAddressTemporary,
            entity.HireDate,
            entity.ResignDate,
            entity.EmergencyContactName,
            entity.EmergencyContactPhone,
            entity.EmergencyContactAddress,
            entity.Qualification,
            entity.AcademicTitle,
            entity.WorkLocation,
            entity.TimekeepingCode,
            entity.BankAccount,
            entity.BankName,
            entity.BankBranch,
            entity.HeightCm,
            entity.WeightKg,
            entity.BloodType,
            entity.BaseSalary,
            entity.TotalSalary,
            entity.AnnualLeaveDays,
            entity.RemainingLeaveDays,
            entity.CompensatoryLeaveDays,
            entity.SeniorityLeaveDays,
            entity.SocialInsuranceStartDate,
            entity.MedicalRegistrationPlace,
            entity.IsCommunistParty,
            entity.CommunistPartyJoinDate,
            entity.IsYouthUnion,
            entity.YouthUnionJoinDate,
            entity.IsTradeUnion,
            entity.TradeUnionJoinDate,
            entity.PhotoUrl,
            entity.Notes,
            entity.CreatedAt,
            entity.UpdatedAt,
            entity.WorkHistories.Select(w => new EmployeeWorkHistoryDto(
                w.Id, w.CompanyName, w.CompanyAddress, w.Industry,
                w.FromDate, w.ToDate, w.JobTitle, w.JobDescription, w.ResignReason)).ToList(),
            entity.Educations.Select(ed => new EmployeeEducationDto(
                ed.Id, ed.SchoolName, ed.Major, ed.DegreeLevel, ed.EducationMode, ed.GradeLevel,
                ed.FromDate, ed.ToDate, ed.CertificateIssueDate, ed.Notes)).ToList(),
            entity.FamilyRelations.Select(f => new EmployeeFamilyRelationDto(
                f.Id, f.FullName, f.Relationship,
                f.BirthYear, f.Occupation, f.CurrentAddress, f.Phone)).ToList(),
            entity.Skills.Select(s => new EmployeeSkillDto(
                s.Id, s.Kind, s.Name, s.LanguageId, s.Level)).ToList(),
            entity.Documents.Select(doc => new EmployeeDocumentDto(
                doc.Id, doc.DocumentType,
                doc.FileName, doc.FilePath, doc.FileSize, doc.ContentType,
                doc.IssueDate, doc.ExpiryDate, doc.Notes)).ToList());
    }
}

// ========== LIST (paged) ==========

// PagedRequest provides Page/PageSize/Search/SortDesc (pattern PE List).
// Spec dùng `PagedQuery` nhưng codebase chỉ có `PagedRequest` — em main confirm
// dùng PagedRequest (Pattern 12-bis mirror PE ListPurchaseEvaluationsQuery line 458).
public record ListEmployeesQuery(
    EmployeeStatus? Status = null,
    Guid? DepartmentId = null) : PagedRequest, IRequest<PagedResult<EmployeeProfileListItemDto>>;

public class ListEmployeesQueryHandler(
    IApplicationDbContext db) : IRequestHandler<ListEmployeesQuery, PagedResult<EmployeeProfileListItemDto>>
{
    public async Task<PagedResult<EmployeeProfileListItemDto>> Handle(
        ListEmployeesQuery request, CancellationToken ct)
    {
        // JOIN Users for FullName/Email + Departments for DepartmentName (LEFT join
        // Departments — User.DepartmentId nullable). Mirror PE Plan AG4 JOIN pattern.
        var q = from e in db.EmployeeProfiles.AsNoTracking().Where(x => !x.IsDeleted)
                join u in db.Users.AsNoTracking() on e.UserId equals u.Id
                join d in db.Departments.AsNoTracking() on u.DepartmentId equals d.Id into dj
                from d in dj.DefaultIfEmpty()
                select new { e, u, d };

        if (request.Status is not null)
            q = q.Where(x => x.e.EmployeeStatus == request.Status);
        if (request.DepartmentId is not null)
            q = q.Where(x => x.u.DepartmentId == request.DepartmentId);

        if (!string.IsNullOrWhiteSpace(request.Search))
        {
            var s = request.Search.Trim();
            q = q.Where(x =>
                x.e.EmployeeCode.Contains(s) ||
                x.u.FullName.Contains(s));
        }

        q = request.SortDesc
            ? q.OrderByDescending(x => x.e.CreatedAt)
            : q.OrderBy(x => x.e.CreatedAt);

        var total = await q.CountAsync(ct);
        var items = await q
            .Skip((request.Page - 1) * request.PageSize).Take(request.PageSize)
            .Select(x => new EmployeeProfileListItemDto(
                x.e.Id,
                x.e.EmployeeCode,
                x.e.UserId,
                x.u.FullName,
                x.u.Email,
                x.u.DepartmentId,
                x.d != null ? x.d.Name : null,
                x.e.EmployeeStatus,
                x.e.Phone,
                x.e.HireDate,
                x.e.CreatedAt,
                x.e.UpdatedAt))
            .ToListAsync(ct);

        return new PagedResult<EmployeeProfileListItemDto>(items, total, request.Page, request.PageSize);
    }
}
