using System.Data;
using FluentValidation;
using MediatR;
using Microsoft.EntityFrameworkCore;
using SolutionErp.Application.Common.Exceptions;
using SolutionErp.Application.Common.Interfaces;
using SolutionErp.Domain.ApprovalWorkflowsV2;
using SolutionErp.Domain.Hrm;
using SolutionErp.Domain.Office;

namespace SolutionErp.Application.Office;

// Phase 11 P11-A Wave 2a (S41/S42 2026-05-30) — wire ApproveV2 CQRS cho LeaveRequest + OtRequest.
// Cookie-cutter mirror ProposalFeatures Region 2 (Mig 38). Schema sẵn (Mig 41).
// ApplicableType: LeaveRequest=5, OtRequest=6.
//
// Per module 7 handler: GetById detail · UpdateDraft · Submit · Approve(UPSERT+advance) ·
//   Reject(TuChoi) · Return(TraLai). 1 static helper GenerateMaDonTuAsync dùng chung.
//
// CONSTRAINT: KHÔNG sửa WorkflowAppsFeatures.cs (Region 1 Create/List ở đó). KHÔNG đụng
//   Travel/Vehicle (Wave 2b song song). MaDonTu gen lần Submit đầu (null→gen).

// ===== Shared CodeGen helper (dùng chung Leave + Ot trong file này) =====

internal static class WorkflowAppCodeGen
{
    // Mirror ProposalFeatures.GenerateMaDeXuatAsync — Serializable tx + Prefix-keyed sequence.
    // Format: "{prefix}/{seq:D3}" — prefix vd "DT/LR/2026" → "DT/LR/2026/001".
    internal static async Task<string> GenerateMaDonTuAsync(
        IApplicationDbContext db, string prefix, int year, IDateTime clock, CancellationToken ct)
    {
        var fullPrefix = $"{prefix}/{year}";
        var dbContext = (DbContext)db;
        await using var tx = await dbContext.Database.BeginTransactionAsync(IsolationLevel.Serializable, ct);
        var seq = await db.WorkflowAppCodeSequences.FirstOrDefaultAsync(s => s.Prefix == fullPrefix, ct);
        if (seq is null)
        {
            seq = new WorkflowAppCodeSequence { Prefix = fullPrefix, LastSeq = 0, UpdatedAt = clock.UtcNow };
            db.WorkflowAppCodeSequences.Add(seq);
        }
        seq.LastSeq++;
        seq.UpdatedAt = clock.UtcNow;
        await db.SaveChangesAsync(ct);
        await tx.CommitAsync(ct);
        return $"{fullPrefix}/{seq.LastSeq:D3}";
    }
}

// =========================================================================
// MODULE A: LeaveRequest (ApplicableType=5, prefix "DT/LR")
// =========================================================================

public record LeaveRequestLevelOpinionDto(
    Guid Id,
    Guid ApprovalWorkflowLevelId,
    int? StepOrder,
    string? StepName,
    int? LevelOrder,
    Guid? ApproverUserId,
    string? Comment,
    DateTime SignedAt,
    Guid SignedByUserId,
    string SignedByFullName);

public record LeaveRequestDetailDto(
    Guid Id,
    string? MaDonTu,
    Guid RequesterUserId,
    string RequesterFullName,
    Guid LeaveTypeId,
    DateTime StartDate,
    DateTime EndDate,
    decimal NumDays,
    string Reason,
    int Status,
    Guid? ApprovalWorkflowId,
    string? WorkflowCode,
    string? WorkflowName,
    int? CurrentApprovalLevelOrder,
    int? RejectedFromStatus,
    DateTime CreatedAt,
    List<LeaveRequestLevelOpinionDto> LevelOpinions,
    // P11-B: số dư phép của NGƯỜI TẠO cho (LeaveType, năm của đơn) — embed để approver
    // cũng thấy (khác /my = balance người xem). null nếu loại phép không tồn tại.
    decimal? LeaveBalanceEntitled,
    decimal? LeaveBalanceUsed,
    decimal? LeaveBalanceRemaining);

public record GetLeaveRequestByIdQuery(Guid Id) : IRequest<LeaveRequestDetailDto?>;

public class GetLeaveRequestByIdHandler(IApplicationDbContext db)
    : IRequestHandler<GetLeaveRequestByIdQuery, LeaveRequestDetailDto?>
{
    public async Task<LeaveRequestDetailDto?> Handle(GetLeaveRequestByIdQuery req, CancellationToken ct)
    {
        var p = await db.LeaveRequests.AsNoTracking()
            .Include(x => x.LevelOpinions)
            .FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
        if (p is null) return null;

        string? wfCode = null;
        string? wfName = null;
        if (p.ApprovalWorkflowId.HasValue)
        {
            var wf = await db.ApprovalWorkflows.AsNoTracking()
                .Where(w => w.Id == p.ApprovalWorkflowId.Value)
                .Select(w => new { w.Code, w.Name })
                .FirstOrDefaultAsync(ct);
            wfCode = wf?.Code;
            wfName = wf?.Name;
        }

        var levelIds = p.LevelOpinions.Select(o => o.ApprovalWorkflowLevelId).Distinct().ToList();
        var levels = await db.ApprovalWorkflowSteps.AsNoTracking()
            .Where(s => s.Levels.Any(l => levelIds.Contains(l.Id)))
            .Select(s => new
            {
                s.Id,
                s.Order,
                s.Name,
                Levels = s.Levels.Where(l => levelIds.Contains(l.Id))
                    .Select(l => new { l.Id, l.Order, l.ApproverUserId })
                    .ToList(),
            })
            .ToListAsync(ct);

        var levelLookup = levels.SelectMany(s => s.Levels.Select(l => new { Step = s, Level = l }))
            .ToDictionary(x => x.Level.Id);

        var opinions = p.LevelOpinions
            .Select(o =>
            {
                levelLookup.TryGetValue(o.ApprovalWorkflowLevelId, out var lvl);
                return new LeaveRequestLevelOpinionDto(
                    o.Id,
                    o.ApprovalWorkflowLevelId,
                    lvl?.Step.Order,
                    lvl?.Step.Name,
                    lvl?.Level.Order,
                    lvl?.Level.ApproverUserId,
                    o.Comment,
                    o.SignedAt,
                    o.SignedByUserId,
                    o.SignedByFullName);
            })
            .OrderBy(o => o.StepOrder).ThenBy(o => o.LevelOrder)
            .ToList();

        // P11-B: số dư phép người tạo cho (LeaveType, năm của StartDate) — hiển thị + cảnh báo vượt.
        // Lazy: chưa có row → từ LeaveType.DaysPerYear (Used=0). Remaining = Entitled + Adjustment − Used.
        var balYear = p.StartDate.Year;
        var balRow = await db.LeaveBalances.AsNoTracking()
            .Where(b => b.UserId == p.RequesterUserId && b.LeaveTypeId == p.LeaveTypeId
                && b.Year == balYear && !b.IsDeleted)
            .Select(b => new { b.EntitledDays, b.UsedDays, b.AdjustmentDays })
            .FirstOrDefaultAsync(ct);
        decimal? balEntitled, balUsed, balRemaining;
        if (balRow is not null)
        {
            balEntitled = balRow.EntitledDays + balRow.AdjustmentDays;
            balUsed = balRow.UsedDays;
            balRemaining = balRow.EntitledDays + balRow.AdjustmentDays - balRow.UsedDays;
        }
        else
        {
            var dpy = await db.LeaveTypes.AsNoTracking()
                .Where(t => t.Id == p.LeaveTypeId).Select(t => (decimal?)t.DaysPerYear).FirstOrDefaultAsync(ct);
            balEntitled = dpy;
            balUsed = dpy.HasValue ? 0m : (decimal?)null;
            balRemaining = dpy;
        }

        return new LeaveRequestDetailDto(
            p.Id, p.MaDonTu, p.RequesterUserId, p.RequesterFullName, p.LeaveTypeId,
            p.StartDate, p.EndDate, p.NumDays, p.Reason, (int)p.Status,
            p.ApprovalWorkflowId, wfCode, wfName, p.CurrentApprovalLevelOrder,
            p.RejectedFromStatus.HasValue ? (int)p.RejectedFromStatus.Value : (int?)null,
            p.CreatedAt, opinions, balEntitled, balUsed, balRemaining);
    }
}

public record UpdateLeaveRequestDraftCommand(
    Guid Id,
    Guid LeaveTypeId,
    DateTime StartDate,
    DateTime EndDate,
    decimal NumDays,
    string Reason,
    Guid? ApprovalWorkflowId) : IRequest;

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

public class UpdateLeaveRequestDraftHandler(IApplicationDbContext db, ICurrentUser cu, IDateTime clock)
    : IRequestHandler<UpdateLeaveRequestDraftCommand>
{
    public async Task Handle(UpdateLeaveRequestDraftCommand req, CancellationToken ct)
    {
        var p = await db.LeaveRequests.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
        if (p is null) throw new NotFoundException("LeaveRequest", req.Id);

        var isOwner = p.RequesterUserId == cu.UserId;
        var isAdmin = cu.Roles.Contains("Admin");
        if (!isOwner && !isAdmin)
            throw new ForbiddenException("Chỉ người tạo hoặc Admin được sửa đơn.");
        if (p.Status != WorkflowAppStatus.Nhap && p.Status != WorkflowAppStatus.TraLai)
            throw new ConflictException("Chỉ sửa được khi trạng thái Nháp hoặc Trả lại.");

        if (req.ApprovalWorkflowId.HasValue && req.ApprovalWorkflowId != p.ApprovalWorkflowId)
        {
            var wfType = await db.ApprovalWorkflows.AsNoTracking()
                .Where(w => w.Id == req.ApprovalWorkflowId.Value)
                .Select(w => (int?)w.ApplicableType)
                .FirstOrDefaultAsync(ct);
            if (wfType is null)
                throw new NotFoundException("ApprovalWorkflow", req.ApprovalWorkflowId.Value);
            if (wfType.Value != (int)ApprovalWorkflowApplicableType.LeaveRequest)
                throw new ConflictException("Quy trình duyệt không thuộc loại Đơn nghỉ phép.");
        }

        // P11-B: enforce LeaveTypeId tồn tại nếu đổi (deduction FK→LeaveTypes Restrict).
        if (req.LeaveTypeId != p.LeaveTypeId
            && !await db.LeaveTypes.AsNoTracking().AnyAsync(t => t.Id == req.LeaveTypeId, ct))
            throw new ConflictException("Loại phép không tồn tại.");

        p.LeaveTypeId = req.LeaveTypeId;
        p.StartDate = req.StartDate;
        p.EndDate = req.EndDate;
        p.NumDays = req.NumDays;
        p.Reason = req.Reason.Trim();
        p.ApprovalWorkflowId = req.ApprovalWorkflowId;
        p.UpdatedAt = clock.UtcNow;
        p.UpdatedBy = cu.UserId;
        await db.SaveChangesAsync(ct);
    }
}

public record SubmitLeaveRequestCommand(Guid Id) : IRequest;

public class SubmitLeaveRequestHandler(IApplicationDbContext db, ICurrentUser cu, IDateTime clock)
    : IRequestHandler<SubmitLeaveRequestCommand>
{
    public async Task Handle(SubmitLeaveRequestCommand req, CancellationToken ct)
    {
        var p = await db.LeaveRequests.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
        if (p is null) throw new NotFoundException("LeaveRequest", req.Id);

        var isOwner = p.RequesterUserId == cu.UserId;
        var isAdmin = cu.Roles.Contains("Admin");
        if (!isOwner && !isAdmin)
            throw new ForbiddenException("Chỉ người tạo hoặc Admin được gửi duyệt.");
        if (p.Status != WorkflowAppStatus.Nhap && p.Status != WorkflowAppStatus.TraLai)
            throw new ConflictException("Chỉ gửi duyệt được khi trạng thái Nháp hoặc Trả lại.");
        if (!p.ApprovalWorkflowId.HasValue)
            throw new ConflictException("Chưa chọn quy trình duyệt.");

        var wfType = await db.ApprovalWorkflows.AsNoTracking()
            .Where(w => w.Id == p.ApprovalWorkflowId.Value)
            .Select(w => (int?)w.ApplicableType)
            .FirstOrDefaultAsync(ct);
        if (wfType is null)
            throw new NotFoundException("ApprovalWorkflow", p.ApprovalWorkflowId.Value);
        if (wfType.Value != (int)ApprovalWorkflowApplicableType.LeaveRequest)
            throw new ConflictException("Quy trình duyệt không thuộc loại Đơn nghỉ phép.");

        if (string.IsNullOrEmpty(p.MaDonTu))
            p.MaDonTu = await WorkflowAppCodeGen.GenerateMaDonTuAsync(db, "DT/LR", clock.Now.Year, clock, ct);

        p.Status = WorkflowAppStatus.DaGuiDuyet;
        p.CurrentApprovalLevelOrder = 1;
        p.RejectedFromStatus = null;
        p.UpdatedAt = clock.UtcNow;
        p.UpdatedBy = cu.UserId;
        await db.SaveChangesAsync(ct);
    }
}

public record ApproveLeaveRequestCommand(Guid Id, string? Comment) : IRequest;

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

        var p = await db.LeaveRequests.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
        if (p is null) throw new NotFoundException("LeaveRequest", req.Id);
        if (p.Status != WorkflowAppStatus.DaGuiDuyet)
            throw new ConflictException("Chỉ duyệt được khi trạng thái Đã gửi duyệt.");
        if (!p.ApprovalWorkflowId.HasValue || !p.CurrentApprovalLevelOrder.HasValue)
            throw new ConflictException("Quy trình duyệt chưa pin hoặc thiếu cấp hiện tại.");

        var wf = await db.ApprovalWorkflows.AsNoTracking()
            .Include(w => w.Steps).ThenInclude(s => s.Levels)
            .FirstOrDefaultAsync(w => w.Id == p.ApprovalWorkflowId.Value, ct);
        if (wf is null) throw new NotFoundException("ApprovalWorkflow", p.ApprovalWorkflowId.Value);

        var allLevels = wf.Steps.OrderBy(s => s.Order)
            .SelectMany(s => s.Levels.OrderBy(l => l.Order).Select(l => new { Step = s, Level = l }))
            .ToList();
        if (allLevels.Count == 0)
            throw new ConflictException("Quy trình duyệt không có cấp duyệt.");

        var currentSlot = allLevels.ElementAtOrDefault(p.CurrentApprovalLevelOrder.Value - 1);
        if (currentSlot is null)
            throw new ConflictException($"Cấp duyệt {p.CurrentApprovalLevelOrder.Value} không tồn tại trong quy trình.");

        var isAdmin = cu.Roles.Contains("Admin");
        if (!isAdmin && currentSlot.Level.ApproverUserId != cu.UserId.Value)
            throw new ForbiddenException("Không phải người duyệt của cấp này.");

        var existing = await db.LeaveRequestLevelOpinions
            .FirstOrDefaultAsync(o => o.LeaveRequestId == p.Id && o.ApprovalWorkflowLevelId == currentSlot.Level.Id, ct);
        var commentFinal = string.IsNullOrWhiteSpace(req.Comment)
            ? "(duyệt — không ý kiến)"
            : req.Comment.Trim();
        if (existing is null)
        {
            db.LeaveRequestLevelOpinions.Add(new LeaveRequestLevelOpinion
            {
                LeaveRequestId = p.Id,
                ApprovalWorkflowLevelId = currentSlot.Level.Id,
                Comment = commentFinal,
                SignedAt = clock.UtcNow,
                SignedByUserId = cu.UserId.Value,
                SignedByFullName = cu.FullName ?? "(unknown)",
                CreatedAt = clock.UtcNow,
                CreatedBy = cu.UserId,
            });
        }
        else
        {
            existing.Comment = commentFinal;
            existing.SignedAt = clock.UtcNow;
            existing.SignedByUserId = cu.UserId.Value;
            existing.SignedByFullName = cu.FullName ?? "(unknown)";
            existing.UpdatedAt = clock.UtcNow;
            existing.UpdatedBy = cu.UserId;
        }

        if (p.CurrentApprovalLevelOrder.Value < allLevels.Count)
        {
            p.CurrentApprovalLevelOrder = p.CurrentApprovalLevelOrder.Value + 1;
        }
        else
        {
            p.Status = WorkflowAppStatus.DaDuyet;
            p.CurrentApprovalLevelOrder = null;
            p.UpdatedAt = clock.UtcNow;
            p.UpdatedBy = cu.UserId;

            // P11-B + S56 #3 (lost-update fix): trừ phép khi duyệt cuối — chạy đúng 1 lần
            // (DaDuyet không approve lại, early guard Status != DaGuiDuyet chặn re-approve).
            // Race-free: ExecuteUpdateAsync làm increment server-side dưới row lock (2 lượt
            // duyệt cuối song song serialize, không mất update). Opinion + status + balance
            // gói trong 1 explicit transaction → atomic all-or-nothing như SaveChanges đơn cũ.
            // S56: IsolationLevel.Serializable khớp convention codebase (codegen/Proposal/
            // TravelVehicle) + serialize cả nhánh auto-create row mới (2 lượt đầu-tiên cùng key).
            var year = p.StartDate.Year;
            var dbContext = (DbContext)db;
            await using var tx = await dbContext.Database.BeginTransactionAsync(IsolationLevel.Serializable, ct);

            // STEP 1 — persist opinion-upsert + status=DaDuyet + ENSURE balance row tồn tại.
            // Auto-create: insert UsedDays=0 qua change tracker để STEP 2 increment được.
            var bal = await db.LeaveBalances.FirstOrDefaultAsync(
                b => b.UserId == p.RequesterUserId && b.LeaveTypeId == p.LeaveTypeId && b.Year == year, ct);
            if (bal is null)
            {
                var daysPerYear = await db.LeaveTypes.AsNoTracking()
                    .Where(t => t.Id == p.LeaveTypeId).Select(t => t.DaysPerYear).FirstOrDefaultAsync(ct);
                bal = new LeaveBalance
                {
                    UserId = p.RequesterUserId,
                    LeaveTypeId = p.LeaveTypeId,
                    Year = year,
                    EntitledDays = daysPerYear,
                    UsedDays = 0,
                    AdjustmentDays = 0,
                    CreatedAt = clock.UtcNow,
                    CreatedBy = cu.UserId,
                };
                db.LeaveBalances.Add(bal);
            }
            bal.UpdatedAt = clock.UtcNow;
            bal.UpdatedBy = cu.UserId;
            await db.SaveChangesAsync(ct);   // commit opinion + status + (insert row nếu mới) trong tx

            // STEP 2 — ATOMIC server-side increment (race-free; KHÔNG qua change tracker).
            // bal tracked vẫn giữ UsedDays cũ sau dòng này — AN TOÀN vì không đọc lại bal,
            // handler return ngay. KHÔNG thêm bal.UsedDays += ... (sẽ double-count).
            await db.LeaveBalances
                .Where(b => b.UserId == p.RequesterUserId && b.LeaveTypeId == p.LeaveTypeId && b.Year == year)
                .ExecuteUpdateAsync(s => s.SetProperty(b => b.UsedDays, b => b.UsedDays + p.NumDays), ct);

            await tx.CommitAsync(ct);
            return;   // terminal branch xử lý trọn trong tx riêng — bỏ qua trailing SaveChanges
        }
        p.UpdatedAt = clock.UtcNow;
        p.UpdatedBy = cu.UserId;
        await db.SaveChangesAsync(ct);
    }
}

public record RejectLeaveRequestCommand(Guid Id, string? Comment) : IRequest;

public class RejectLeaveRequestHandler(IApplicationDbContext db, ICurrentUser cu, IDateTime clock)
    : IRequestHandler<RejectLeaveRequestCommand>
{
    public async Task Handle(RejectLeaveRequestCommand req, CancellationToken ct)
    {
        var p = await db.LeaveRequests.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
        if (p is null) throw new NotFoundException("LeaveRequest", req.Id);
        if (p.Status != WorkflowAppStatus.DaGuiDuyet)
            throw new ConflictException("Chỉ từ chối được khi đang trong workflow duyệt.");

        var isAdmin = cu.Roles.Contains("Admin");
        if (!isAdmin && p.CurrentApprovalLevelOrder.HasValue && p.ApprovalWorkflowId.HasValue)
        {
            var wf = await db.ApprovalWorkflows.AsNoTracking()
                .Include(w => w.Steps).ThenInclude(s => s.Levels)
                .FirstOrDefaultAsync(w => w.Id == p.ApprovalWorkflowId.Value, ct);
            var allLevels = wf?.Steps.OrderBy(s => s.Order)
                .SelectMany(s => s.Levels.OrderBy(l => l.Order))
                .ToList() ?? new();
            var currentLevel = allLevels.ElementAtOrDefault(p.CurrentApprovalLevelOrder.Value - 1);
            if (currentLevel?.ApproverUserId != cu.UserId)
                throw new ForbiddenException("Không phải người duyệt của cấp này.");
        }

        p.Status = WorkflowAppStatus.TuChoi;
        p.CurrentApprovalLevelOrder = null;
        p.UpdatedAt = clock.UtcNow;
        p.UpdatedBy = cu.UserId;
        await db.SaveChangesAsync(ct);
    }
}

public record ReturnLeaveRequestCommand(Guid Id, string? Comment) : IRequest;

public class ReturnLeaveRequestHandler(IApplicationDbContext db, ICurrentUser cu, IDateTime clock)
    : IRequestHandler<ReturnLeaveRequestCommand>
{
    public async Task Handle(ReturnLeaveRequestCommand req, CancellationToken ct)
    {
        var p = await db.LeaveRequests.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
        if (p is null) throw new NotFoundException("LeaveRequest", req.Id);
        if (p.Status != WorkflowAppStatus.DaGuiDuyet)
            throw new ConflictException("Chỉ trả lại được khi đang trong workflow duyệt.");

        var isAdmin = cu.Roles.Contains("Admin");
        if (!isAdmin && p.CurrentApprovalLevelOrder.HasValue && p.ApprovalWorkflowId.HasValue)
        {
            var wf = await db.ApprovalWorkflows.AsNoTracking()
                .Include(w => w.Steps).ThenInclude(s => s.Levels)
                .FirstOrDefaultAsync(w => w.Id == p.ApprovalWorkflowId.Value, ct);
            var allLevels = wf?.Steps.OrderBy(s => s.Order)
                .SelectMany(s => s.Levels.OrderBy(l => l.Order))
                .ToList() ?? new();
            var currentLevel = allLevels.ElementAtOrDefault(p.CurrentApprovalLevelOrder.Value - 1);
            if (currentLevel?.ApproverUserId != cu.UserId)
                throw new ForbiddenException("Không phải người duyệt của cấp này.");
        }

        p.Status = WorkflowAppStatus.TraLai;
        p.RejectedFromStatus = WorkflowAppStatus.DaGuiDuyet;
        p.CurrentApprovalLevelOrder = null;
        p.UpdatedAt = clock.UtcNow;
        p.UpdatedBy = cu.UserId;
        await db.SaveChangesAsync(ct);
    }
}

// =========================================================================
// Set workflow (pin quy trình cho draft) — Phase 11 P11-A fix S42.
// Endpoint riêng PUT /{id}/workflow: chỉ set ApprovalWorkflowId trên draft
// Nhap/TraLai (verify ApplicableType). KHÔNG validate field khác (khác UpdateDraft —
// tránh 400 khi FE chỉ gửi workflowId). Single-responsibility: chọn quy trình.
// =========================================================================

public record SetLeaveRequestWorkflowCommand(Guid Id, Guid ApprovalWorkflowId) : IRequest;

public class SetLeaveRequestWorkflowHandler(IApplicationDbContext db, ICurrentUser cu, IDateTime clock)
    : IRequestHandler<SetLeaveRequestWorkflowCommand>
{
    public async Task Handle(SetLeaveRequestWorkflowCommand req, CancellationToken ct)
    {
        var p = await db.LeaveRequests.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
        if (p is null) throw new NotFoundException("LeaveRequest", req.Id);
        var isOwner = p.RequesterUserId == cu.UserId;
        var isAdmin = cu.Roles.Contains("Admin");
        if (!isOwner && !isAdmin)
            throw new ForbiddenException("Chỉ người tạo hoặc Admin được chọn quy trình.");
        if (p.Status != WorkflowAppStatus.Nhap && p.Status != WorkflowAppStatus.TraLai)
            throw new ConflictException("Chỉ chọn quy trình khi trạng thái Nháp hoặc Trả lại.");
        var wfType = await db.ApprovalWorkflows.AsNoTracking()
            .Where(w => w.Id == req.ApprovalWorkflowId)
            .Select(w => (int?)w.ApplicableType).FirstOrDefaultAsync(ct);
        if (wfType is null) throw new NotFoundException("ApprovalWorkflow", req.ApprovalWorkflowId);
        if (wfType.Value != (int)ApprovalWorkflowApplicableType.LeaveRequest)
            throw new ConflictException("Quy trình duyệt không thuộc loại Đơn nghỉ phép.");
        p.ApprovalWorkflowId = req.ApprovalWorkflowId;
        p.UpdatedAt = clock.UtcNow;
        p.UpdatedBy = cu.UserId;
        await db.SaveChangesAsync(ct);
    }
}

public record SetOtRequestWorkflowCommand(Guid Id, Guid ApprovalWorkflowId) : IRequest;

public class SetOtRequestWorkflowHandler(IApplicationDbContext db, ICurrentUser cu, IDateTime clock)
    : IRequestHandler<SetOtRequestWorkflowCommand>
{
    public async Task Handle(SetOtRequestWorkflowCommand req, CancellationToken ct)
    {
        var p = await db.OtRequests.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
        if (p is null) throw new NotFoundException("OtRequest", req.Id);
        var isOwner = p.RequesterUserId == cu.UserId;
        var isAdmin = cu.Roles.Contains("Admin");
        if (!isOwner && !isAdmin)
            throw new ForbiddenException("Chỉ người tạo hoặc Admin được chọn quy trình.");
        if (p.Status != WorkflowAppStatus.Nhap && p.Status != WorkflowAppStatus.TraLai)
            throw new ConflictException("Chỉ chọn quy trình khi trạng thái Nháp hoặc Trả lại.");
        var wfType = await db.ApprovalWorkflows.AsNoTracking()
            .Where(w => w.Id == req.ApprovalWorkflowId)
            .Select(w => (int?)w.ApplicableType).FirstOrDefaultAsync(ct);
        if (wfType is null) throw new NotFoundException("ApprovalWorkflow", req.ApprovalWorkflowId);
        if (wfType.Value != (int)ApprovalWorkflowApplicableType.OtRequest)
            throw new ConflictException("Quy trình duyệt không thuộc loại Đơn OT.");
        p.ApprovalWorkflowId = req.ApprovalWorkflowId;
        p.UpdatedAt = clock.UtcNow;
        p.UpdatedBy = cu.UserId;
        await db.SaveChangesAsync(ct);
    }
}

// =========================================================================
// MODULE B: OtRequest (ApplicableType=6, prefix "DT/OT")
// =========================================================================

public record OtRequestLevelOpinionDto(
    Guid Id,
    Guid ApprovalWorkflowLevelId,
    int? StepOrder,
    string? StepName,
    int? LevelOrder,
    Guid? ApproverUserId,
    string? Comment,
    DateTime SignedAt,
    Guid SignedByUserId,
    string SignedByFullName);

public record OtRequestDetailDto(
    Guid Id,
    string? MaDonTu,
    Guid RequesterUserId,
    string RequesterFullName,
    DateTime OtDate,
    TimeSpan StartTime,
    TimeSpan EndTime,
    decimal Hours,
    string Reason,
    Guid? OtPolicyId,
    int Status,
    Guid? ApprovalWorkflowId,
    string? WorkflowCode,
    string? WorkflowName,
    int? CurrentApprovalLevelOrder,
    int? RejectedFromStatus,
    DateTime CreatedAt,
    List<OtRequestLevelOpinionDto> LevelOpinions);

public record GetOtRequestByIdQuery(Guid Id) : IRequest<OtRequestDetailDto?>;

public class GetOtRequestByIdHandler(IApplicationDbContext db)
    : IRequestHandler<GetOtRequestByIdQuery, OtRequestDetailDto?>
{
    public async Task<OtRequestDetailDto?> Handle(GetOtRequestByIdQuery req, CancellationToken ct)
    {
        var p = await db.OtRequests.AsNoTracking()
            .Include(x => x.LevelOpinions)
            .FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
        if (p is null) return null;

        string? wfCode = null;
        string? wfName = null;
        if (p.ApprovalWorkflowId.HasValue)
        {
            var wf = await db.ApprovalWorkflows.AsNoTracking()
                .Where(w => w.Id == p.ApprovalWorkflowId.Value)
                .Select(w => new { w.Code, w.Name })
                .FirstOrDefaultAsync(ct);
            wfCode = wf?.Code;
            wfName = wf?.Name;
        }

        var levelIds = p.LevelOpinions.Select(o => o.ApprovalWorkflowLevelId).Distinct().ToList();
        var levels = await db.ApprovalWorkflowSteps.AsNoTracking()
            .Where(s => s.Levels.Any(l => levelIds.Contains(l.Id)))
            .Select(s => new
            {
                s.Id,
                s.Order,
                s.Name,
                Levels = s.Levels.Where(l => levelIds.Contains(l.Id))
                    .Select(l => new { l.Id, l.Order, l.ApproverUserId })
                    .ToList(),
            })
            .ToListAsync(ct);

        var levelLookup = levels.SelectMany(s => s.Levels.Select(l => new { Step = s, Level = l }))
            .ToDictionary(x => x.Level.Id);

        var opinions = p.LevelOpinions
            .Select(o =>
            {
                levelLookup.TryGetValue(o.ApprovalWorkflowLevelId, out var lvl);
                return new OtRequestLevelOpinionDto(
                    o.Id,
                    o.ApprovalWorkflowLevelId,
                    lvl?.Step.Order,
                    lvl?.Step.Name,
                    lvl?.Level.Order,
                    lvl?.Level.ApproverUserId,
                    o.Comment,
                    o.SignedAt,
                    o.SignedByUserId,
                    o.SignedByFullName);
            })
            .OrderBy(o => o.StepOrder).ThenBy(o => o.LevelOrder)
            .ToList();

        return new OtRequestDetailDto(
            p.Id, p.MaDonTu, p.RequesterUserId, p.RequesterFullName,
            p.OtDate, p.StartTime, p.EndTime, p.Hours, p.Reason, p.OtPolicyId, (int)p.Status,
            p.ApprovalWorkflowId, wfCode, wfName, p.CurrentApprovalLevelOrder,
            p.RejectedFromStatus.HasValue ? (int)p.RejectedFromStatus.Value : (int?)null,
            p.CreatedAt, opinions);
    }
}

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

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

public class UpdateOtRequestDraftHandler(IApplicationDbContext db, ICurrentUser cu, IDateTime clock)
    : IRequestHandler<UpdateOtRequestDraftCommand>
{
    public async Task Handle(UpdateOtRequestDraftCommand req, CancellationToken ct)
    {
        var p = await db.OtRequests.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
        if (p is null) throw new NotFoundException("OtRequest", req.Id);

        var isOwner = p.RequesterUserId == cu.UserId;
        var isAdmin = cu.Roles.Contains("Admin");
        if (!isOwner && !isAdmin)
            throw new ForbiddenException("Chỉ người tạo hoặc Admin được sửa đơn.");
        if (p.Status != WorkflowAppStatus.Nhap && p.Status != WorkflowAppStatus.TraLai)
            throw new ConflictException("Chỉ sửa được khi trạng thái Nháp hoặc Trả lại.");

        if (req.ApprovalWorkflowId.HasValue && req.ApprovalWorkflowId != p.ApprovalWorkflowId)
        {
            var wfType = await db.ApprovalWorkflows.AsNoTracking()
                .Where(w => w.Id == req.ApprovalWorkflowId.Value)
                .Select(w => (int?)w.ApplicableType)
                .FirstOrDefaultAsync(ct);
            if (wfType is null)
                throw new NotFoundException("ApprovalWorkflow", req.ApprovalWorkflowId.Value);
            if (wfType.Value != (int)ApprovalWorkflowApplicableType.OtRequest)
                throw new ConflictException("Quy trình duyệt không thuộc loại Đơn OT.");
        }

        p.OtDate = req.OtDate;
        p.StartTime = req.StartTime;
        p.EndTime = req.EndTime;
        p.Hours = req.Hours;
        p.Reason = req.Reason.Trim();
        p.OtPolicyId = req.OtPolicyId;
        p.ApprovalWorkflowId = req.ApprovalWorkflowId;
        p.UpdatedAt = clock.UtcNow;
        p.UpdatedBy = cu.UserId;
        await db.SaveChangesAsync(ct);
    }
}

public record SubmitOtRequestCommand(Guid Id) : IRequest;

public class SubmitOtRequestHandler(IApplicationDbContext db, ICurrentUser cu, IDateTime clock)
    : IRequestHandler<SubmitOtRequestCommand>
{
    public async Task Handle(SubmitOtRequestCommand req, CancellationToken ct)
    {
        var p = await db.OtRequests.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
        if (p is null) throw new NotFoundException("OtRequest", req.Id);

        var isOwner = p.RequesterUserId == cu.UserId;
        var isAdmin = cu.Roles.Contains("Admin");
        if (!isOwner && !isAdmin)
            throw new ForbiddenException("Chỉ người tạo hoặc Admin được gửi duyệt.");
        if (p.Status != WorkflowAppStatus.Nhap && p.Status != WorkflowAppStatus.TraLai)
            throw new ConflictException("Chỉ gửi duyệt được khi trạng thái Nháp hoặc Trả lại.");
        if (!p.ApprovalWorkflowId.HasValue)
            throw new ConflictException("Chưa chọn quy trình duyệt.");

        var wfType = await db.ApprovalWorkflows.AsNoTracking()
            .Where(w => w.Id == p.ApprovalWorkflowId.Value)
            .Select(w => (int?)w.ApplicableType)
            .FirstOrDefaultAsync(ct);
        if (wfType is null)
            throw new NotFoundException("ApprovalWorkflow", p.ApprovalWorkflowId.Value);
        if (wfType.Value != (int)ApprovalWorkflowApplicableType.OtRequest)
            throw new ConflictException("Quy trình duyệt không thuộc loại Đơn OT.");

        if (string.IsNullOrEmpty(p.MaDonTu))
            p.MaDonTu = await WorkflowAppCodeGen.GenerateMaDonTuAsync(db, "DT/OT", clock.Now.Year, clock, ct);

        p.Status = WorkflowAppStatus.DaGuiDuyet;
        p.CurrentApprovalLevelOrder = 1;
        p.RejectedFromStatus = null;
        p.UpdatedAt = clock.UtcNow;
        p.UpdatedBy = cu.UserId;
        await db.SaveChangesAsync(ct);
    }
}

public record ApproveOtRequestCommand(Guid Id, string? Comment) : IRequest;

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

        var p = await db.OtRequests.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
        if (p is null) throw new NotFoundException("OtRequest", req.Id);
        if (p.Status != WorkflowAppStatus.DaGuiDuyet)
            throw new ConflictException("Chỉ duyệt được khi trạng thái Đã gửi duyệt.");
        if (!p.ApprovalWorkflowId.HasValue || !p.CurrentApprovalLevelOrder.HasValue)
            throw new ConflictException("Quy trình duyệt chưa pin hoặc thiếu cấp hiện tại.");

        var wf = await db.ApprovalWorkflows.AsNoTracking()
            .Include(w => w.Steps).ThenInclude(s => s.Levels)
            .FirstOrDefaultAsync(w => w.Id == p.ApprovalWorkflowId.Value, ct);
        if (wf is null) throw new NotFoundException("ApprovalWorkflow", p.ApprovalWorkflowId.Value);

        var allLevels = wf.Steps.OrderBy(s => s.Order)
            .SelectMany(s => s.Levels.OrderBy(l => l.Order).Select(l => new { Step = s, Level = l }))
            .ToList();
        if (allLevels.Count == 0)
            throw new ConflictException("Quy trình duyệt không có cấp duyệt.");

        var currentSlot = allLevels.ElementAtOrDefault(p.CurrentApprovalLevelOrder.Value - 1);
        if (currentSlot is null)
            throw new ConflictException($"Cấp duyệt {p.CurrentApprovalLevelOrder.Value} không tồn tại trong quy trình.");

        var isAdmin = cu.Roles.Contains("Admin");
        if (!isAdmin && currentSlot.Level.ApproverUserId != cu.UserId.Value)
            throw new ForbiddenException("Không phải người duyệt của cấp này.");

        var existing = await db.OtRequestLevelOpinions
            .FirstOrDefaultAsync(o => o.OtRequestId == p.Id && o.ApprovalWorkflowLevelId == currentSlot.Level.Id, ct);
        var commentFinal = string.IsNullOrWhiteSpace(req.Comment)
            ? "(duyệt — không ý kiến)"
            : req.Comment.Trim();
        if (existing is null)
        {
            db.OtRequestLevelOpinions.Add(new OtRequestLevelOpinion
            {
                OtRequestId = p.Id,
                ApprovalWorkflowLevelId = currentSlot.Level.Id,
                Comment = commentFinal,
                SignedAt = clock.UtcNow,
                SignedByUserId = cu.UserId.Value,
                SignedByFullName = cu.FullName ?? "(unknown)",
                CreatedAt = clock.UtcNow,
                CreatedBy = cu.UserId,
            });
        }
        else
        {
            existing.Comment = commentFinal;
            existing.SignedAt = clock.UtcNow;
            existing.SignedByUserId = cu.UserId.Value;
            existing.SignedByFullName = cu.FullName ?? "(unknown)";
            existing.UpdatedAt = clock.UtcNow;
            existing.UpdatedBy = cu.UserId;
        }

        if (p.CurrentApprovalLevelOrder.Value < allLevels.Count)
        {
            p.CurrentApprovalLevelOrder = p.CurrentApprovalLevelOrder.Value + 1;
        }
        else
        {
            p.Status = WorkflowAppStatus.DaDuyet;
            p.CurrentApprovalLevelOrder = null;
        }
        p.UpdatedAt = clock.UtcNow;
        p.UpdatedBy = cu.UserId;
        await db.SaveChangesAsync(ct);
    }
}

public record RejectOtRequestCommand(Guid Id, string? Comment) : IRequest;

public class RejectOtRequestHandler(IApplicationDbContext db, ICurrentUser cu, IDateTime clock)
    : IRequestHandler<RejectOtRequestCommand>
{
    public async Task Handle(RejectOtRequestCommand req, CancellationToken ct)
    {
        var p = await db.OtRequests.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
        if (p is null) throw new NotFoundException("OtRequest", req.Id);
        if (p.Status != WorkflowAppStatus.DaGuiDuyet)
            throw new ConflictException("Chỉ từ chối được khi đang trong workflow duyệt.");

        var isAdmin = cu.Roles.Contains("Admin");
        if (!isAdmin && p.CurrentApprovalLevelOrder.HasValue && p.ApprovalWorkflowId.HasValue)
        {
            var wf = await db.ApprovalWorkflows.AsNoTracking()
                .Include(w => w.Steps).ThenInclude(s => s.Levels)
                .FirstOrDefaultAsync(w => w.Id == p.ApprovalWorkflowId.Value, ct);
            var allLevels = wf?.Steps.OrderBy(s => s.Order)
                .SelectMany(s => s.Levels.OrderBy(l => l.Order))
                .ToList() ?? new();
            var currentLevel = allLevels.ElementAtOrDefault(p.CurrentApprovalLevelOrder.Value - 1);
            if (currentLevel?.ApproverUserId != cu.UserId)
                throw new ForbiddenException("Không phải người duyệt của cấp này.");
        }

        p.Status = WorkflowAppStatus.TuChoi;
        p.CurrentApprovalLevelOrder = null;
        p.UpdatedAt = clock.UtcNow;
        p.UpdatedBy = cu.UserId;
        await db.SaveChangesAsync(ct);
    }
}

public record ReturnOtRequestCommand(Guid Id, string? Comment) : IRequest;

public class ReturnOtRequestHandler(IApplicationDbContext db, ICurrentUser cu, IDateTime clock)
    : IRequestHandler<ReturnOtRequestCommand>
{
    public async Task Handle(ReturnOtRequestCommand req, CancellationToken ct)
    {
        var p = await db.OtRequests.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
        if (p is null) throw new NotFoundException("OtRequest", req.Id);
        if (p.Status != WorkflowAppStatus.DaGuiDuyet)
            throw new ConflictException("Chỉ trả lại được khi đang trong workflow duyệt.");

        var isAdmin = cu.Roles.Contains("Admin");
        if (!isAdmin && p.CurrentApprovalLevelOrder.HasValue && p.ApprovalWorkflowId.HasValue)
        {
            var wf = await db.ApprovalWorkflows.AsNoTracking()
                .Include(w => w.Steps).ThenInclude(s => s.Levels)
                .FirstOrDefaultAsync(w => w.Id == p.ApprovalWorkflowId.Value, ct);
            var allLevels = wf?.Steps.OrderBy(s => s.Order)
                .SelectMany(s => s.Levels.OrderBy(l => l.Order))
                .ToList() ?? new();
            var currentLevel = allLevels.ElementAtOrDefault(p.CurrentApprovalLevelOrder.Value - 1);
            if (currentLevel?.ApproverUserId != cu.UserId)
                throw new ForbiddenException("Không phải người duyệt của cấp này.");
        }

        p.Status = WorkflowAppStatus.TraLai;
        p.RejectedFromStatus = WorkflowAppStatus.DaGuiDuyet;
        p.CurrentApprovalLevelOrder = null;
        p.UpdatedAt = clock.UtcNow;
        p.UpdatedBy = cu.UserId;
        await db.SaveChangesAsync(ct);
    }
}
