using FluentValidation;
using MediatR;
using Microsoft.EntityFrameworkCore;
using SolutionErp.Application.Common.Exceptions;
using SolutionErp.Application.Common.Interfaces;
using SolutionErp.Application.Common.Models;
using SolutionErp.Domain.Office;

namespace SolutionErp.Application.Office;

// Phase 10.3-10.4 G-O4+G-O5+G-O6+G-P1 (Mig 39+40 — S38 2026-05-28).
// SKELETON Phase 1 — UAT visible cho 5 module Workflow Apps + Attendance.
// Scope: Create + List + GetById + Update draft only.
// DEFER Phase 11: ApproveV2 workflow advance + LevelOpinions + CodeGen atomic
// + LeaveBalance business logic + Overlap check + Auto-assign + SLA timer + Monthly report.

// =========================================================================
// REGION 1: LeaveRequest (G-O4)
// =========================================================================

public record LeaveRequestDto(Guid Id, string? MaDonTu, Guid RequesterUserId, string RequesterFullName,
    Guid LeaveTypeId, DateTime StartDate, DateTime EndDate, decimal NumDays, string Reason,
    int Status, Guid? ApprovalWorkflowId, int? CurrentApprovalLevelOrder, DateTime CreatedAt);

public record CreateLeaveRequestCommand(Guid LeaveTypeId, DateTime StartDate, DateTime EndDate,
    decimal NumDays, string Reason, Guid? ApprovalWorkflowId) : IRequest<Guid>;

public class CreateLeaveRequestValidator : AbstractValidator<CreateLeaveRequestCommand>
{
    public CreateLeaveRequestValidator()
    {
        RuleFor(x => x.Reason).NotEmpty().MaximumLength(1000);
        RuleFor(x => x.NumDays).GreaterThan(0);
        RuleFor(x => x.EndDate).GreaterThanOrEqualTo(x => x.StartDate);
    }
}

public class CreateLeaveRequestHandler(IApplicationDbContext db, ICurrentUser cu, IDateTime clock)
    : IRequestHandler<CreateLeaveRequestCommand, Guid>
{
    public async Task<Guid> Handle(CreateLeaveRequestCommand req, CancellationToken ct)
    {
        if (cu.UserId is null) throw new UnauthorizedException();
        // P11-B: enforce LeaveTypeId tồn tại (deduction lúc duyệt cuối insert LeaveBalance
        // FK→LeaveTypes Restrict — bogus type → 500 kẹt đơn). Guard tại cửa Create.
        if (!await db.LeaveTypes.AsNoTracking().AnyAsync(t => t.Id == req.LeaveTypeId, ct))
            throw new ConflictException("Loại phép không tồn tại.");
        var e = new LeaveRequest
        {
            RequesterUserId = cu.UserId.Value,
            RequesterFullName = cu.FullName ?? "(unknown)",
            LeaveTypeId = req.LeaveTypeId,
            StartDate = req.StartDate,
            EndDate = req.EndDate,
            NumDays = req.NumDays,
            Reason = req.Reason.Trim(),
            ApprovalWorkflowId = req.ApprovalWorkflowId,
            Status = WorkflowAppStatus.Nhap,
            CreatedAt = clock.UtcNow,
            CreatedBy = cu.UserId,
        };
        db.LeaveRequests.Add(e);
        await db.SaveChangesAsync(ct);
        return e.Id;
    }
}

public record GetLeaveRequestsQuery(int? Status, Guid? RequesterUserId, int Page = 1, int PageSize = 50)
    : IRequest<PagedResult<LeaveRequestDto>>;

public class GetLeaveRequestsHandler(IApplicationDbContext db)
    : IRequestHandler<GetLeaveRequestsQuery, PagedResult<LeaveRequestDto>>
{
    public async Task<PagedResult<LeaveRequestDto>> Handle(GetLeaveRequestsQuery q, CancellationToken ct)
    {
        var page = q.Page < 1 ? 1 : q.Page;
        var pageSize = q.PageSize is < 1 or > 200 ? 50 : q.PageSize;
        var query = db.LeaveRequests.AsNoTracking().Where(x => !x.IsDeleted);
        if (q.Status.HasValue) query = query.Where(x => (int)x.Status == q.Status.Value);
        if (q.RequesterUserId.HasValue) query = query.Where(x => x.RequesterUserId == q.RequesterUserId.Value);
        var total = await query.CountAsync(ct);
        var items = await query.OrderByDescending(x => x.CreatedAt).Skip((page - 1) * pageSize).Take(pageSize)
            .Select(x => new LeaveRequestDto(x.Id, x.MaDonTu, x.RequesterUserId, x.RequesterFullName,
                x.LeaveTypeId, x.StartDate, x.EndDate, x.NumDays, x.Reason,
                (int)x.Status, x.ApprovalWorkflowId, x.CurrentApprovalLevelOrder, x.CreatedAt))
            .ToListAsync(ct);
        return new PagedResult<LeaveRequestDto>(items, total, page, pageSize);
    }
}

// =========================================================================
// REGION 2: OtRequest (G-O4)
// =========================================================================

public record OtRequestDto(Guid Id, string? MaDonTu, Guid RequesterUserId, string RequesterFullName,
    DateTime OtDate, TimeSpan StartTime, TimeSpan EndTime, decimal Hours, string Reason,
    Guid? OtPolicyId, int Status, Guid? ApprovalWorkflowId, int? CurrentApprovalLevelOrder, DateTime CreatedAt);

public record CreateOtRequestCommand(DateTime OtDate, TimeSpan StartTime, TimeSpan EndTime,
    decimal Hours, string Reason, Guid? OtPolicyId, Guid? ApprovalWorkflowId) : IRequest<Guid>;

public class CreateOtRequestValidator : AbstractValidator<CreateOtRequestCommand>
{
    public CreateOtRequestValidator()
    {
        RuleFor(x => x.Reason).NotEmpty().MaximumLength(1000);
        RuleFor(x => x.Hours).GreaterThan(0);
        RuleFor(x => x.EndTime).GreaterThan(x => x.StartTime);
    }
}

public class CreateOtRequestHandler(IApplicationDbContext db, ICurrentUser cu, IDateTime clock)
    : IRequestHandler<CreateOtRequestCommand, Guid>
{
    public async Task<Guid> Handle(CreateOtRequestCommand req, CancellationToken ct)
    {
        if (cu.UserId is null) throw new UnauthorizedException();
        var e = new OtRequest
        {
            RequesterUserId = cu.UserId.Value,
            RequesterFullName = cu.FullName ?? "(unknown)",
            OtDate = req.OtDate,
            StartTime = req.StartTime,
            EndTime = req.EndTime,
            Hours = req.Hours,
            Reason = req.Reason.Trim(),
            OtPolicyId = req.OtPolicyId,
            ApprovalWorkflowId = req.ApprovalWorkflowId,
            Status = WorkflowAppStatus.Nhap,
            CreatedAt = clock.UtcNow,
            CreatedBy = cu.UserId,
        };
        db.OtRequests.Add(e);
        await db.SaveChangesAsync(ct);
        return e.Id;
    }
}

public record GetOtRequestsQuery(int? Status, Guid? RequesterUserId, int Page = 1, int PageSize = 50)
    : IRequest<PagedResult<OtRequestDto>>;

public class GetOtRequestsHandler(IApplicationDbContext db)
    : IRequestHandler<GetOtRequestsQuery, PagedResult<OtRequestDto>>
{
    public async Task<PagedResult<OtRequestDto>> Handle(GetOtRequestsQuery q, CancellationToken ct)
    {
        var page = q.Page < 1 ? 1 : q.Page;
        var pageSize = q.PageSize is < 1 or > 200 ? 50 : q.PageSize;
        var query = db.OtRequests.AsNoTracking().Where(x => !x.IsDeleted);
        if (q.Status.HasValue) query = query.Where(x => (int)x.Status == q.Status.Value);
        if (q.RequesterUserId.HasValue) query = query.Where(x => x.RequesterUserId == q.RequesterUserId.Value);
        var total = await query.CountAsync(ct);
        var items = await query.OrderByDescending(x => x.CreatedAt).Skip((page - 1) * pageSize).Take(pageSize)
            .Select(x => new OtRequestDto(x.Id, x.MaDonTu, x.RequesterUserId, x.RequesterFullName,
                x.OtDate, x.StartTime, x.EndTime, x.Hours, x.Reason, x.OtPolicyId,
                (int)x.Status, x.ApprovalWorkflowId, x.CurrentApprovalLevelOrder, x.CreatedAt))
            .ToListAsync(ct);
        return new PagedResult<OtRequestDto>(items, total, page, pageSize);
    }
}

// =========================================================================
// REGION 3: TravelRequest (G-O4)
// =========================================================================

public record TravelRequestDto(Guid Id, string? MaDonTu, Guid RequesterUserId, string RequesterFullName,
    string Destination, DateTime StartDate, DateTime EndDate, int NumDays, string Purpose,
    decimal? EstimatedCost, int Status, Guid? ApprovalWorkflowId, int? CurrentApprovalLevelOrder, DateTime CreatedAt);

public record CreateTravelRequestCommand(string Destination, DateTime StartDate, DateTime EndDate,
    int NumDays, string Purpose, decimal? EstimatedCost, Guid? ApprovalWorkflowId) : IRequest<Guid>;

public class CreateTravelRequestValidator : AbstractValidator<CreateTravelRequestCommand>
{
    public CreateTravelRequestValidator()
    {
        RuleFor(x => x.Destination).NotEmpty().MaximumLength(300);
        RuleFor(x => x.Purpose).NotEmpty().MaximumLength(1000);
        RuleFor(x => x.NumDays).GreaterThan(0);
        RuleFor(x => x.EstimatedCost).GreaterThanOrEqualTo(0).When(x => x.EstimatedCost.HasValue);
    }
}

public class CreateTravelRequestHandler(IApplicationDbContext db, ICurrentUser cu, IDateTime clock)
    : IRequestHandler<CreateTravelRequestCommand, Guid>
{
    public async Task<Guid> Handle(CreateTravelRequestCommand req, CancellationToken ct)
    {
        if (cu.UserId is null) throw new UnauthorizedException();
        var e = new TravelRequest
        {
            RequesterUserId = cu.UserId.Value,
            RequesterFullName = cu.FullName ?? "(unknown)",
            Destination = req.Destination.Trim(),
            StartDate = req.StartDate,
            EndDate = req.EndDate,
            NumDays = req.NumDays,
            Purpose = req.Purpose.Trim(),
            EstimatedCost = req.EstimatedCost,
            ApprovalWorkflowId = req.ApprovalWorkflowId,
            Status = WorkflowAppStatus.Nhap,
            CreatedAt = clock.UtcNow,
            CreatedBy = cu.UserId,
        };
        db.TravelRequests.Add(e);
        await db.SaveChangesAsync(ct);
        return e.Id;
    }
}

public record GetTravelRequestsQuery(int? Status, Guid? RequesterUserId, int Page = 1, int PageSize = 50)
    : IRequest<PagedResult<TravelRequestDto>>;

public class GetTravelRequestsHandler(IApplicationDbContext db)
    : IRequestHandler<GetTravelRequestsQuery, PagedResult<TravelRequestDto>>
{
    public async Task<PagedResult<TravelRequestDto>> Handle(GetTravelRequestsQuery q, CancellationToken ct)
    {
        var page = q.Page < 1 ? 1 : q.Page;
        var pageSize = q.PageSize is < 1 or > 200 ? 50 : q.PageSize;
        var query = db.TravelRequests.AsNoTracking().Where(x => !x.IsDeleted);
        if (q.Status.HasValue) query = query.Where(x => (int)x.Status == q.Status.Value);
        if (q.RequesterUserId.HasValue) query = query.Where(x => x.RequesterUserId == q.RequesterUserId.Value);
        var total = await query.CountAsync(ct);
        var items = await query.OrderByDescending(x => x.CreatedAt).Skip((page - 1) * pageSize).Take(pageSize)
            .Select(x => new TravelRequestDto(x.Id, x.MaDonTu, x.RequesterUserId, x.RequesterFullName,
                x.Destination, x.StartDate, x.EndDate, x.NumDays, x.Purpose, x.EstimatedCost,
                (int)x.Status, x.ApprovalWorkflowId, x.CurrentApprovalLevelOrder, x.CreatedAt))
            .ToListAsync(ct);
        return new PagedResult<TravelRequestDto>(items, total, page, pageSize);
    }
}

// =========================================================================
// REGION 4: VehicleBooking (G-O5)
// =========================================================================

public record VehicleBookingDto(Guid Id, string? MaDonTu, Guid RequesterUserId, string RequesterFullName,
    string VehicleLicense, string? VehicleName, DateTime StartAt, DateTime EndAt,
    string Destination, string Purpose, string? DriverName,
    int Status, Guid? ApprovalWorkflowId, int? CurrentApprovalLevelOrder, DateTime CreatedAt);

public record CreateVehicleBookingCommand(string VehicleLicense, string? VehicleName,
    DateTime StartAt, DateTime EndAt, string Destination, string Purpose, string? DriverName,
    Guid? ApprovalWorkflowId) : IRequest<Guid>;

public class CreateVehicleBookingValidator : AbstractValidator<CreateVehicleBookingCommand>
{
    public CreateVehicleBookingValidator()
    {
        RuleFor(x => x.VehicleLicense).NotEmpty().MaximumLength(20);
        RuleFor(x => x.Destination).NotEmpty().MaximumLength(300);
        RuleFor(x => x.Purpose).NotEmpty().MaximumLength(1000);
        RuleFor(x => x.EndAt).GreaterThan(x => x.StartAt);
    }
}

public class CreateVehicleBookingHandler(IApplicationDbContext db, ICurrentUser cu, IDateTime clock)
    : IRequestHandler<CreateVehicleBookingCommand, Guid>
{
    public async Task<Guid> Handle(CreateVehicleBookingCommand req, CancellationToken ct)
    {
        if (cu.UserId is null) throw new UnauthorizedException();
        var e = new VehicleBooking
        {
            RequesterUserId = cu.UserId.Value,
            RequesterFullName = cu.FullName ?? "(unknown)",
            VehicleLicense = req.VehicleLicense.Trim(),
            VehicleName = req.VehicleName?.Trim(),
            StartAt = req.StartAt,
            EndAt = req.EndAt,
            Destination = req.Destination.Trim(),
            Purpose = req.Purpose.Trim(),
            DriverName = req.DriverName?.Trim(),
            ApprovalWorkflowId = req.ApprovalWorkflowId,
            Status = WorkflowAppStatus.Nhap,
            CreatedAt = clock.UtcNow,
            CreatedBy = cu.UserId,
        };
        db.VehicleBookings.Add(e);
        await db.SaveChangesAsync(ct);
        return e.Id;
    }
}

public record GetVehicleBookingsQuery(int? Status, Guid? RequesterUserId, int Page = 1, int PageSize = 50)
    : IRequest<PagedResult<VehicleBookingDto>>;

public class GetVehicleBookingsHandler(IApplicationDbContext db)
    : IRequestHandler<GetVehicleBookingsQuery, PagedResult<VehicleBookingDto>>
{
    public async Task<PagedResult<VehicleBookingDto>> Handle(GetVehicleBookingsQuery q, CancellationToken ct)
    {
        var page = q.Page < 1 ? 1 : q.Page;
        var pageSize = q.PageSize is < 1 or > 200 ? 50 : q.PageSize;
        var query = db.VehicleBookings.AsNoTracking().Where(x => !x.IsDeleted);
        if (q.Status.HasValue) query = query.Where(x => (int)x.Status == q.Status.Value);
        if (q.RequesterUserId.HasValue) query = query.Where(x => x.RequesterUserId == q.RequesterUserId.Value);
        var total = await query.CountAsync(ct);
        var items = await query.OrderByDescending(x => x.CreatedAt).Skip((page - 1) * pageSize).Take(pageSize)
            .Select(x => new VehicleBookingDto(x.Id, x.MaDonTu, x.RequesterUserId, x.RequesterFullName,
                x.VehicleLicense, x.VehicleName, x.StartAt, x.EndAt, x.Destination, x.Purpose, x.DriverName,
                (int)x.Status, x.ApprovalWorkflowId, x.CurrentApprovalLevelOrder, x.CreatedAt))
            .ToListAsync(ct);
        return new PagedResult<VehicleBookingDto>(items, total, page, pageSize);
    }
}

// =========================================================================
// REGION 5: ItTicket (G-O6) — kanban status (NO workflow V2)
// =========================================================================

public record ItTicketDto(Guid Id, string? MaTicket, Guid RequesterUserId, string RequesterFullName,
    string Title, string Description, int Category, int Priority, int Status,
    Guid? AssignedToUserId, string? AssignedToFullName, DateTime? ResolvedAt, string? Resolution, DateTime CreatedAt,
    DateTime? SlaDueAt, bool SlaBreached);

public record CreateItTicketCommand(string Title, string Description, int Category, int Priority) : IRequest<Guid>;

public class CreateItTicketValidator : AbstractValidator<CreateItTicketCommand>
{
    public CreateItTicketValidator()
    {
        RuleFor(x => x.Title).NotEmpty().MaximumLength(300);
        RuleFor(x => x.Description).NotEmpty().MaximumLength(5000);
        RuleFor(x => x.Category).GreaterThan(0);
        RuleFor(x => x.Priority).InclusiveBetween(1, 4);
    }
}

public class CreateItTicketHandler(IApplicationDbContext db, ICurrentUser cu, IDateTime clock)
    : IRequestHandler<CreateItTicketCommand, Guid>
{
    // P11-D (Mig 46) — SLA window theo Priority. Source-of-truth shared với
    // ItTicketSlaJob (warning/breach tính cùng map). Urgent gấp nhất → 4h.
    public static readonly IReadOnlyDictionary<ItTicketPriority, TimeSpan> SlaWindow =
        new Dictionary<ItTicketPriority, TimeSpan>
        {
            [ItTicketPriority.Urgent] = TimeSpan.FromHours(4),
            [ItTicketPriority.High] = TimeSpan.FromHours(8),
            [ItTicketPriority.Medium] = TimeSpan.FromHours(24),
            [ItTicketPriority.Low] = TimeSpan.FromHours(72),
        };

    public async Task<Guid> Handle(CreateItTicketCommand req, CancellationToken ct)
    {
        if (cu.UserId is null) throw new UnauthorizedException();
        var priority = (ItTicketPriority)req.Priority;
        var e = new ItTicket
        {
            RequesterUserId = cu.UserId.Value,
            RequesterFullName = cu.FullName ?? "(unknown)",
            Title = req.Title.Trim(),
            Description = req.Description.Trim(),
            Category = (ItTicketCategory)req.Category,
            Priority = priority,
            Status = ItTicketStatus.Open,
            CreatedAt = clock.UtcNow,
            CreatedBy = cu.UserId,
        };
        // P11-F: gen mã ticket lúc Create (ItTicket = kanban KHÔNG workflow,
        // khác Leave/OT gen lúc Submit). Format "IT/2026/001" (Serializable tx atomic).
        e.MaTicket = await WorkflowAppCodeGen.GenerateMaDonTuAsync(db, "IT", clock.Now.Year, clock, ct);

        // P11-D: SLA due = CreatedAt + window theo Priority (default Medium 24h nếu lạ).
        e.SlaDueAt = e.CreatedAt + (SlaWindow.TryGetValue(priority, out var w) ? w : TimeSpan.FromHours(24));

        // P11-D: round-robin least-loaded — assign cho IT staff (Department.Code=="IT")
        // ít ticket-mở nhất. Tie-break theo Id (deterministic). Không có ai trong IT
        // → để unassigned (admin assign tay sau qua /assign).
        var itDeptId = await db.Departments.AsNoTracking()
            .Where(d => d.Code == "IT" && !d.IsDeleted)
            .Select(d => (Guid?)d.Id)
            .FirstOrDefaultAsync(ct);
        if (itDeptId is Guid deptId)
        {
            var assignee = await db.Users
                .Where(u => u.DepartmentId == deptId && u.IsActive)
                .OrderBy(u => db.ItTickets.Count(t => t.AssignedToUserId == u.Id
                    && t.Status != ItTicketStatus.Closed && t.Status != ItTicketStatus.Resolved && !t.IsDeleted))
                .ThenBy(u => u.Id)
                .FirstOrDefaultAsync(ct);
            if (assignee is not null)
            {
                e.AssignedToUserId = assignee.Id;
                e.AssignedToFullName = assignee.FullName;
            }
        }

        db.ItTickets.Add(e);
        await db.SaveChangesAsync(ct);
        return e.Id;
    }
}

public record GetItTicketsQuery(int? Status, int? Category, int? Priority, Guid? AssignedToUserId,
    Guid? RequesterUserId, int Page = 1, int PageSize = 50) : IRequest<PagedResult<ItTicketDto>>;

public class GetItTicketsHandler(IApplicationDbContext db)
    : IRequestHandler<GetItTicketsQuery, PagedResult<ItTicketDto>>
{
    public async Task<PagedResult<ItTicketDto>> Handle(GetItTicketsQuery q, CancellationToken ct)
    {
        var page = q.Page < 1 ? 1 : q.Page;
        var pageSize = q.PageSize is < 1 or > 200 ? 50 : q.PageSize;
        var query = db.ItTickets.AsNoTracking().Where(x => !x.IsDeleted);
        if (q.Status.HasValue) query = query.Where(x => (int)x.Status == q.Status.Value);
        if (q.Category.HasValue) query = query.Where(x => (int)x.Category == q.Category.Value);
        if (q.Priority.HasValue) query = query.Where(x => (int)x.Priority == q.Priority.Value);
        if (q.AssignedToUserId.HasValue) query = query.Where(x => x.AssignedToUserId == q.AssignedToUserId.Value);
        if (q.RequesterUserId.HasValue) query = query.Where(x => x.RequesterUserId == q.RequesterUserId.Value);
        var total = await query.CountAsync(ct);
        var items = await query.OrderByDescending(x => x.CreatedAt).Skip((page - 1) * pageSize).Take(pageSize)
            .Select(x => new ItTicketDto(x.Id, x.MaTicket, x.RequesterUserId, x.RequesterFullName,
                x.Title, x.Description, (int)x.Category, (int)x.Priority, (int)x.Status,
                x.AssignedToUserId, x.AssignedToFullName, x.ResolvedAt, x.Resolution, x.CreatedAt,
                x.SlaDueAt, x.SlaBreached))
            .ToListAsync(ct);
        return new PagedResult<ItTicketDto>(items, total, page, pageSize);
    }
}

public record UpdateItTicketStatusCommand(Guid Id, int Status, string? Resolution) : IRequest;

public class UpdateItTicketStatusHandler(IApplicationDbContext db, ICurrentUser cu, IDateTime clock)
    : IRequestHandler<UpdateItTicketStatusCommand>
{
    public async Task Handle(UpdateItTicketStatusCommand req, CancellationToken ct)
    {
        if (cu.UserId is null) throw new UnauthorizedException();
        var t = await db.ItTickets.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
        if (t is null) throw new NotFoundException("ItTicket", req.Id);
        t.Status = (ItTicketStatus)req.Status;
        if (req.Resolution != null) t.Resolution = req.Resolution.Trim();
        if (t.Status == ItTicketStatus.Resolved && t.ResolvedAt is null)
            t.ResolvedAt = clock.UtcNow;
        t.UpdatedAt = clock.UtcNow;
        t.UpdatedBy = cu.UserId;
        await db.SaveChangesAsync(ct);
    }
}

// S54: danh sách nhân viên tổ IT có thể nhận ticket + cờ canReassign (Admin HOẶC thành viên dept IT).
// FE gate nút "Đổi người xử lý" theo CanReassign (BE-computed, tránh silent 403 gotcha #44).
public record AssignableStaffDto(Guid Id, string FullName);
public record AssignableStaffResult(bool CanReassign, IReadOnlyList<AssignableStaffDto> Staff);
public record GetAssignableItStaffQuery : IRequest<AssignableStaffResult>;

public class GetAssignableItStaffHandler(IApplicationDbContext db, ICurrentUser cu)
    : IRequestHandler<GetAssignableItStaffQuery, AssignableStaffResult>
{
    public async Task<AssignableStaffResult> Handle(GetAssignableItStaffQuery q, CancellationToken ct)
    {
        if (cu.UserId is null) throw new UnauthorizedException();
        var itDeptId = await db.Departments.AsNoTracking()
            .Where(d => d.Code == "IT" && !d.IsDeleted)
            .Select(d => (Guid?)d.Id)
            .FirstOrDefaultAsync(ct);
        var isAdmin = cu.Roles.Contains("Admin");
        var myDeptId = await db.Users.AsNoTracking()
            .Where(u => u.Id == cu.UserId.Value)
            .Select(u => u.DepartmentId)
            .FirstOrDefaultAsync(ct);
        var isItStaff = itDeptId is Guid d && myDeptId == d;
        var canReassign = isAdmin || isItStaff;
        if (!canReassign || itDeptId is not Guid deptId)
            return new AssignableStaffResult(canReassign, Array.Empty<AssignableStaffDto>());
        var staff = await db.Users.AsNoTracking()
            .Where(u => u.DepartmentId == deptId && u.IsActive)
            .OrderBy(u => u.FullName)
            .Select(u => new AssignableStaffDto(u.Id, u.FullName))
            .ToListAsync(ct);
        return new AssignableStaffResult(canReassign, staff);
    }
}

// P11-D: admin re-assign ticket cho IT staff cụ thể (override round-robin auto-assign
// lúc Create). Denorm AssignedToFullName từ User.FullName tại thời điểm gán.
public record AssignItTicketCommand(Guid Id, Guid AssignedToUserId) : IRequest;

public class AssignItTicketValidator : AbstractValidator<AssignItTicketCommand>
{
    public AssignItTicketValidator()
    {
        RuleFor(x => x.Id).NotEmpty();
        RuleFor(x => x.AssignedToUserId).NotEmpty();
    }
}

public class AssignItTicketHandler(IApplicationDbContext db, ICurrentUser cu, IDateTime clock)
    : IRequestHandler<AssignItTicketCommand>
{
    public async Task Handle(AssignItTicketCommand req, CancellationToken ct)
    {
        if (cu.UserId is null) throw new UnauthorizedException();

        // S54 + S56 #5: authz check TRƯỚC khi tra ticket — fail-closed. Non-Admin/non-IT
        // caller nhận ForbiddenException cho MỌI ticketId (tồn tại hay không), tránh
        // existence-oracle leak (NotFound vs Forbidden tiết lộ ticket có thật).
        var itDeptId = await db.Departments.AsNoTracking()
            .Where(d => d.Code == "IT" && !d.IsDeleted)
            .Select(d => (Guid?)d.Id)
            .FirstOrDefaultAsync(ct);
        var isAdmin = cu.Roles.Contains("Admin");
        var myDeptId = await db.Users.AsNoTracking()
            .Where(u => u.Id == cu.UserId.Value).Select(u => u.DepartmentId).FirstOrDefaultAsync(ct);
        if (!isAdmin && !(itDeptId is Guid mine && myDeptId == mine))
            throw new ForbiddenException("Chỉ Admin hoặc nhân viên tổ IT mới được gán lại ticket.");

        var t = await db.ItTickets.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
        if (t is null) throw new NotFoundException("ItTicket", req.Id);

        var assignee = await db.Users.AsNoTracking()
            .FirstOrDefaultAsync(u => u.Id == req.AssignedToUserId && u.IsActive, ct);
        if (assignee is null) throw new NotFoundException("User", req.AssignedToUserId);
        // S54: assignee bắt buộc thuộc tổ IT (khớp dropdown scoped + round-robin).
        if (!(itDeptId is Guid itd && assignee.DepartmentId == itd))
            throw new ConflictException("Người được giao phải thuộc tổ IT.");
        t.AssignedToUserId = assignee.Id;
        t.AssignedToFullName = assignee.FullName;
        t.UpdatedAt = clock.UtcNow;
        t.UpdatedBy = cu.UserId;
        await db.SaveChangesAsync(ct);
    }
}

// =========================================================================
// REGION 6: Attendance (G-P1)
// =========================================================================

public record AttendanceDto(Guid Id, Guid UserId, string UserFullName, DateTime AttendanceDate,
    DateTime? CheckInAt, DateTime? CheckOutAt, int SourceIn, int SourceOut,
    decimal? CheckInLatitude, decimal? CheckInLongitude, decimal? WorkHours, decimal? OtHours, string? Note);

public record CheckInCommand(decimal? Latitude, decimal? Longitude, decimal? Accuracy, string? Note) : IRequest<Guid>;

public class CheckInHandler(IApplicationDbContext db, ICurrentUser cu, IDateTime clock)
    : IRequestHandler<CheckInCommand, Guid>
{
    public async Task<Guid> Handle(CheckInCommand req, CancellationToken ct)
    {
        if (cu.UserId is null) throw new UnauthorizedException();
        var today = clock.Now.Date;
        var existing = await db.Attendances.FirstOrDefaultAsync(x => x.UserId == cu.UserId.Value && x.AttendanceDate == today, ct);
        if (existing is not null)
        {
            if (existing.CheckInAt.HasValue)
                throw new ConflictException("Đã check-in hôm nay.");
            existing.CheckInAt = clock.UtcNow;
            existing.CheckInLatitude = req.Latitude;
            existing.CheckInLongitude = req.Longitude;
            existing.CheckInAccuracy = req.Accuracy;
            existing.SourceIn = AttendanceSource.Web;
            existing.Note = req.Note?.Trim();
            existing.UpdatedAt = clock.UtcNow;
            existing.UpdatedBy = cu.UserId;
            await db.SaveChangesAsync(ct);
            return existing.Id;
        }
        var e = new Attendance
        {
            UserId = cu.UserId.Value,
            UserFullName = cu.FullName ?? "(unknown)",
            AttendanceDate = today,
            CheckInAt = clock.UtcNow,
            CheckInLatitude = req.Latitude,
            CheckInLongitude = req.Longitude,
            CheckInAccuracy = req.Accuracy,
            SourceIn = AttendanceSource.Web,
            SourceOut = AttendanceSource.Web,
            Note = req.Note?.Trim(),
            CreatedAt = clock.UtcNow,
            CreatedBy = cu.UserId,
        };
        db.Attendances.Add(e);
        await db.SaveChangesAsync(ct);
        return e.Id;
    }
}

public record CheckOutCommand(decimal? Latitude, decimal? Longitude, decimal? Accuracy) : IRequest;

public class CheckOutHandler(IApplicationDbContext db, ICurrentUser cu, IDateTime clock)
    : IRequestHandler<CheckOutCommand>
{
    public async Task Handle(CheckOutCommand req, CancellationToken ct)
    {
        if (cu.UserId is null) throw new UnauthorizedException();
        var today = clock.Now.Date;
        var att = await db.Attendances.FirstOrDefaultAsync(x => x.UserId == cu.UserId.Value && x.AttendanceDate == today, ct);
        if (att is null) throw new NotFoundException("Attendance", today);
        if (!att.CheckInAt.HasValue)
            throw new ConflictException("Chưa check-in hôm nay.");
        att.CheckOutAt = clock.UtcNow;
        att.CheckOutLatitude = req.Latitude;
        att.CheckOutLongitude = req.Longitude;
        att.CheckOutAccuracy = req.Accuracy;
        att.SourceOut = AttendanceSource.Web;
        // Simple WorkHours calc: diff in hours
        if (att.CheckOutAt.HasValue && att.CheckInAt.HasValue)
            att.WorkHours = (decimal)(att.CheckOutAt.Value - att.CheckInAt.Value).TotalHours;
        att.UpdatedAt = clock.UtcNow;
        att.UpdatedBy = cu.UserId;
        await db.SaveChangesAsync(ct);
    }
}

public record GetMyAttendanceQuery(int Year, int Month) : IRequest<List<AttendanceDto>>;

public class GetMyAttendanceHandler(IApplicationDbContext db, ICurrentUser cu)
    : IRequestHandler<GetMyAttendanceQuery, List<AttendanceDto>>
{
    public async Task<List<AttendanceDto>> Handle(GetMyAttendanceQuery q, CancellationToken ct)
    {
        if (cu.UserId is null) throw new UnauthorizedException();
        var monthStart = new DateTime(q.Year, q.Month, 1);
        var monthEnd = monthStart.AddMonths(1);
        return await db.Attendances.AsNoTracking()
            .Where(x => x.UserId == cu.UserId.Value && x.AttendanceDate >= monthStart && x.AttendanceDate < monthEnd && !x.IsDeleted)
            .OrderBy(x => x.AttendanceDate)
            .Select(x => new AttendanceDto(x.Id, x.UserId, x.UserFullName, x.AttendanceDate,
                x.CheckInAt, x.CheckOutAt, (int)x.SourceIn, (int)x.SourceOut,
                x.CheckInLatitude, x.CheckInLongitude, x.WorkHours, x.OtHours, x.Note))
            .ToListAsync(ct);
    }
}

// =========================================================================
// REGION 7: HR Dashboard (G-H3)
// =========================================================================

public record HrDashboardDto(int TotalEmployees, int ActiveEmployees, int OnLeaveEmployees, int ResignedEmployees,
    int MaleCount, int FemaleCount, int BirthdaysThisWeek, int NewHiresThisMonth);

public record GetHrDashboardQuery : IRequest<HrDashboardDto>;

public class GetHrDashboardHandler(IApplicationDbContext db, IDateTime clock)
    : IRequestHandler<GetHrDashboardQuery, HrDashboardDto>
{
    public async Task<HrDashboardDto> Handle(GetHrDashboardQuery q, CancellationToken ct)
    {
        var now = clock.Now;
        var weekEnd = now.AddDays(7);
        var monthStart = new DateTime(now.Year, now.Month, 1);

        var total = await db.EmployeeProfiles.CountAsync(x => !x.IsDeleted, ct);
        var active = await db.EmployeeProfiles.CountAsync(x => !x.IsDeleted && (int)x.EmployeeStatus == 1, ct);
        var onLeave = await db.EmployeeProfiles.CountAsync(x => !x.IsDeleted && (int)x.EmployeeStatus == 2, ct);
        var resigned = await db.EmployeeProfiles.CountAsync(x => !x.IsDeleted && (int)x.EmployeeStatus == 3, ct);
        var male = await db.EmployeeProfiles.CountAsync(x => !x.IsDeleted && x.Gender == Domain.Hrm.Gender.Male, ct);
        var female = await db.EmployeeProfiles.CountAsync(x => !x.IsDeleted && x.Gender == Domain.Hrm.Gender.Female, ct);

        // Birthdays in next 7 days — compare month+day
        var birthdays = await db.EmployeeProfiles.AsNoTracking()
            .Where(x => !x.IsDeleted && x.DateOfBirth.HasValue)
            .Select(x => x.DateOfBirth!.Value)
            .ToListAsync(ct);
        var bdaysThisWeek = birthdays.Count(d =>
        {
            var thisYearBday = new DateTime(now.Year, d.Month, d.Day);
            return thisYearBday >= now.Date && thisYearBday <= weekEnd.Date;
        });
        _ = bdaysThisWeek; // silence unused if

        var monthStartDateOnly = DateOnly.FromDateTime(monthStart);
        var newHires = await db.EmployeeProfiles.CountAsync(x => !x.IsDeleted && x.HireDate.HasValue && x.HireDate >= monthStartDateOnly, ct);

        return new HrDashboardDto(total, active, onLeave, resigned, male, female, bdaysThisWeek, newHires);
    }
}
