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.Office;

namespace SolutionErp.Application.Office;

// Phase 11 P11-A Wave 2b (S41 2026-05-30) — Wire ApproveV2 CQRS cho TravelRequest + VehicleBooking.
// Cookie-cutter mirror ProposalFeatures Region 2 (Mig 38). Schema Wave 1 (Mig 41) đã sẵn:
//   TravelRequestLevelOpinions + VehicleBookingLevelOpinions + WorkflowAppCodeSequences
//   parent +RejectedFromStatus + nav LevelOpinions.
//
// ApplicableType: TravelRequest=9 (Mig 41 mới) · VehicleBooking=7.
// WorkflowAppStatus {Nhap=1, DaGuiDuyet=2, TraLai=3, TuChoi=4, DaDuyet=5}.
//
// Endpoint per module (qua {Travel,Vehicle}*Controller):
//   GET    /{id}          — detail Include LevelOpinions + Workflow metadata
//   PUT    /{id}          — update draft (Nhap or TraLai only)
//   POST   /{id}/submit   — gen MaDonTu atomic + Status=DaGuiDuyet
//   POST   /{id}/approve  — ApproveV2: UPSERT LevelOpinion + advance level/terminal
//   POST   /{id}/reject   — Status=TuChoi terminal (no opinion sync)
//   POST   /{id}/return   — Status=TraLai + RejectedFromStatus=DaGuiDuyet (no opinion sync)
//
// Note: GetList + Create giữ nguyên trong WorkflowAppsFeatures.cs (KHÔNG sửa file đó).

// =========================================================================
// REGION 1: TravelRequest — ApproveV2 wire
// =========================================================================

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

public record TravelRequestDetailDto(
    Guid Id,
    string? MaDonTu,
    Guid RequesterUserId,
    string RequesterFullName,
    string Destination,
    DateTime StartDate,
    DateTime EndDate,
    int NumDays,
    string Purpose,
    decimal? EstimatedCost,
    int Status,
    Guid? ApprovalWorkflowId,
    string? WorkflowCode,
    string? WorkflowName,
    int? CurrentApprovalLevelOrder,
    int? RejectedFromStatus,
    DateTime CreatedAt,
    List<TravelRequestLevelOpinionDto> LevelOpinions);

public record GetTravelRequestByIdQuery(Guid Id) : IRequest<TravelRequestDetailDto?>;

public class GetTravelRequestByIdHandler(IApplicationDbContext db)
    : IRequestHandler<GetTravelRequestByIdQuery, TravelRequestDetailDto?>
{
    public async Task<TravelRequestDetailDto?> Handle(GetTravelRequestByIdQuery req, CancellationToken ct)
    {
        var p = await db.TravelRequests.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 TravelRequestLevelOpinionDto(
                    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 TravelRequestDetailDto(
            p.Id, p.MaDonTu, p.RequesterUserId, p.RequesterFullName,
            p.Destination, p.StartDate, p.EndDate, p.NumDays, p.Purpose, p.EstimatedCost,
            (int)p.Status, p.ApprovalWorkflowId, wfCode, wfName, p.CurrentApprovalLevelOrder,
            p.RejectedFromStatus.HasValue ? (int)p.RejectedFromStatus.Value : (int?)null,
            p.CreatedAt, opinions);
    }
}

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

public class UpdateTravelRequestDraftValidator : AbstractValidator<UpdateTravelRequestDraftCommand>
{
    public UpdateTravelRequestDraftValidator()
    {
        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 UpdateTravelRequestDraftHandler(IApplicationDbContext db, ICurrentUser currentUser, IDateTime clock)
    : IRequestHandler<UpdateTravelRequestDraftCommand>
{
    public async Task Handle(UpdateTravelRequestDraftCommand req, CancellationToken ct)
    {
        var p = await db.TravelRequests.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
        if (p is null) throw new NotFoundException("TravelRequest", req.Id);

        var isOwner = p.RequesterUserId == currentUser.UserId;
        var isAdmin = currentUser.Roles.Contains("Admin");
        if (!isOwner && !isAdmin)
            throw new ForbiddenException("Chỉ người tạo hoặc Admin được sửa đơn công tác.");
        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.TravelRequest)
                throw new ConflictException("Quy trình duyệt không thuộc loại Đơn công tác.");
        }

        p.Destination = req.Destination.Trim();
        p.StartDate = req.StartDate;
        p.EndDate = req.EndDate;
        p.NumDays = req.NumDays;
        p.Purpose = req.Purpose.Trim();
        p.EstimatedCost = req.EstimatedCost;
        p.ApprovalWorkflowId = req.ApprovalWorkflowId;
        p.UpdatedAt = clock.UtcNow;
        p.UpdatedBy = currentUser.UserId;
        await db.SaveChangesAsync(ct);
    }
}

public record SubmitTravelRequestCommand(Guid Id) : IRequest;

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

        var isOwner = p.RequesterUserId == currentUser.UserId;
        var isAdmin = currentUser.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.TravelRequest)
            throw new ConflictException("Quy trình duyệt không thuộc loại Đơn công tác.");

        if (string.IsNullOrEmpty(p.MaDonTu))
        {
            p.MaDonTu = await TravelVehicleCodeGen.GenerateMaDonTuAsync(
                db, $"DT/CT/{clock.Now.Year}", clock, ct);
        }

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

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

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

        var p = await db.TravelRequests.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
        if (p is null) throw new NotFoundException("TravelRequest", 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 = currentUser.Roles.Contains("Admin");
        if (!isAdmin && currentSlot.Level.ApproverUserId != currentUser.UserId.Value)
            throw new ForbiddenException("Không phải người duyệt của cấp này.");

        var existing = await db.TravelRequestLevelOpinions
            .FirstOrDefaultAsync(o => o.TravelRequestId == 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.TravelRequestLevelOpinions.Add(new TravelRequestLevelOpinion
            {
                TravelRequestId = p.Id,
                ApprovalWorkflowLevelId = currentSlot.Level.Id,
                Comment = commentFinal,
                SignedAt = clock.UtcNow,
                SignedByUserId = currentUser.UserId.Value,
                SignedByFullName = currentUser.FullName ?? "(unknown)",
                CreatedAt = clock.UtcNow,
                CreatedBy = currentUser.UserId,
            });
        }
        else
        {
            existing.Comment = commentFinal;
            existing.SignedAt = clock.UtcNow;
            existing.SignedByUserId = currentUser.UserId.Value;
            existing.SignedByFullName = currentUser.FullName ?? "(unknown)";
            existing.UpdatedAt = clock.UtcNow;
            existing.UpdatedBy = currentUser.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 = currentUser.UserId;
        await db.SaveChangesAsync(ct);
    }
}

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

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

        var isAdmin = currentUser.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 != currentUser.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 = currentUser.UserId;
        await db.SaveChangesAsync(ct);
    }
}

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

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

        var isAdmin = currentUser.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 != currentUser.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 = currentUser.UserId;
        await db.SaveChangesAsync(ct);
    }
}

// =========================================================================
// REGION 2: VehicleBooking — ApproveV2 wire
// =========================================================================

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

public record VehicleBookingDetailDto(
    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,
    string? WorkflowCode,
    string? WorkflowName,
    int? CurrentApprovalLevelOrder,
    int? RejectedFromStatus,
    DateTime CreatedAt,
    List<VehicleBookingLevelOpinionDto> LevelOpinions);

public record GetVehicleBookingByIdQuery(Guid Id) : IRequest<VehicleBookingDetailDto?>;

public class GetVehicleBookingByIdHandler(IApplicationDbContext db)
    : IRequestHandler<GetVehicleBookingByIdQuery, VehicleBookingDetailDto?>
{
    public async Task<VehicleBookingDetailDto?> Handle(GetVehicleBookingByIdQuery req, CancellationToken ct)
    {
        var p = await db.VehicleBookings.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 VehicleBookingLevelOpinionDto(
                    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 VehicleBookingDetailDto(
            p.Id, p.MaDonTu, p.RequesterUserId, p.RequesterFullName,
            p.VehicleLicense, p.VehicleName, p.StartAt, p.EndAt, p.Destination, p.Purpose, p.DriverName,
            (int)p.Status, p.ApprovalWorkflowId, wfCode, wfName, p.CurrentApprovalLevelOrder,
            p.RejectedFromStatus.HasValue ? (int)p.RejectedFromStatus.Value : (int?)null,
            p.CreatedAt, opinions);
    }
}

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

public class UpdateVehicleBookingDraftValidator : AbstractValidator<UpdateVehicleBookingDraftCommand>
{
    public UpdateVehicleBookingDraftValidator()
    {
        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 UpdateVehicleBookingDraftHandler(IApplicationDbContext db, ICurrentUser currentUser, IDateTime clock)
    : IRequestHandler<UpdateVehicleBookingDraftCommand>
{
    public async Task Handle(UpdateVehicleBookingDraftCommand req, CancellationToken ct)
    {
        var p = await db.VehicleBookings.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
        if (p is null) throw new NotFoundException("VehicleBooking", req.Id);

        var isOwner = p.RequesterUserId == currentUser.UserId;
        var isAdmin = currentUser.Roles.Contains("Admin");
        if (!isOwner && !isAdmin)
            throw new ForbiddenException("Chỉ người tạo hoặc Admin được sửa đơn đặt xe.");
        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.VehicleBooking)
                throw new ConflictException("Quy trình duyệt không thuộc loại Đặt xe.");
        }

        p.VehicleLicense = req.VehicleLicense.Trim();
        p.VehicleName = req.VehicleName?.Trim();
        p.StartAt = req.StartAt;
        p.EndAt = req.EndAt;
        p.Destination = req.Destination.Trim();
        p.Purpose = req.Purpose.Trim();
        p.DriverName = req.DriverName?.Trim();
        p.ApprovalWorkflowId = req.ApprovalWorkflowId;
        p.UpdatedAt = clock.UtcNow;
        p.UpdatedBy = currentUser.UserId;
        await db.SaveChangesAsync(ct);
    }
}

public record SubmitVehicleBookingCommand(Guid Id) : IRequest;

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

        var isOwner = p.RequesterUserId == currentUser.UserId;
        var isAdmin = currentUser.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.VehicleBooking)
            throw new ConflictException("Quy trình duyệt không thuộc loại Đặt xe.");

        if (string.IsNullOrEmpty(p.MaDonTu))
        {
            p.MaDonTu = await TravelVehicleCodeGen.GenerateMaDonTuAsync(
                db, $"DX/XE/{clock.Now.Year}", clock, ct);
        }

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

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

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

        var p = await db.VehicleBookings.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
        if (p is null) throw new NotFoundException("VehicleBooking", 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 = currentUser.Roles.Contains("Admin");
        if (!isAdmin && currentSlot.Level.ApproverUserId != currentUser.UserId.Value)
            throw new ForbiddenException("Không phải người duyệt của cấp này.");

        var existing = await db.VehicleBookingLevelOpinions
            .FirstOrDefaultAsync(o => o.VehicleBookingId == 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.VehicleBookingLevelOpinions.Add(new VehicleBookingLevelOpinion
            {
                VehicleBookingId = p.Id,
                ApprovalWorkflowLevelId = currentSlot.Level.Id,
                Comment = commentFinal,
                SignedAt = clock.UtcNow,
                SignedByUserId = currentUser.UserId.Value,
                SignedByFullName = currentUser.FullName ?? "(unknown)",
                CreatedAt = clock.UtcNow,
                CreatedBy = currentUser.UserId,
            });
        }
        else
        {
            existing.Comment = commentFinal;
            existing.SignedAt = clock.UtcNow;
            existing.SignedByUserId = currentUser.UserId.Value;
            existing.SignedByFullName = currentUser.FullName ?? "(unknown)";
            existing.UpdatedAt = clock.UtcNow;
            existing.UpdatedBy = currentUser.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 = currentUser.UserId;
        await db.SaveChangesAsync(ct);
    }
}

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

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

        var isAdmin = currentUser.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 != currentUser.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 = currentUser.UserId;
        await db.SaveChangesAsync(ct);
    }
}

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

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

        var isAdmin = currentUser.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 != currentUser.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 = currentUser.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).
// =========================================================================

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

public class SetTravelRequestWorkflowHandler(IApplicationDbContext db, ICurrentUser currentUser, IDateTime clock)
    : IRequestHandler<SetTravelRequestWorkflowCommand>
{
    public async Task Handle(SetTravelRequestWorkflowCommand req, CancellationToken ct)
    {
        var p = await db.TravelRequests.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
        if (p is null) throw new NotFoundException("TravelRequest", req.Id);
        var isOwner = p.RequesterUserId == currentUser.UserId;
        var isAdmin = currentUser.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.TravelRequest)
            throw new ConflictException("Quy trình duyệt không thuộc loại Đơn công tác.");
        p.ApprovalWorkflowId = req.ApprovalWorkflowId;
        p.UpdatedAt = clock.UtcNow;
        p.UpdatedBy = currentUser.UserId;
        await db.SaveChangesAsync(ct);
    }
}

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

public class SetVehicleBookingWorkflowHandler(IApplicationDbContext db, ICurrentUser currentUser, IDateTime clock)
    : IRequestHandler<SetVehicleBookingWorkflowCommand>
{
    public async Task Handle(SetVehicleBookingWorkflowCommand req, CancellationToken ct)
    {
        var p = await db.VehicleBookings.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
        if (p is null) throw new NotFoundException("VehicleBooking", req.Id);
        var isOwner = p.RequesterUserId == currentUser.UserId;
        var isAdmin = currentUser.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.VehicleBooking)
            throw new ConflictException("Quy trình duyệt không thuộc loại Đặt xe.");
        p.ApprovalWorkflowId = req.ApprovalWorkflowId;
        p.UpdatedAt = clock.UtcNow;
        p.UpdatedBy = currentUser.UserId;
        await db.SaveChangesAsync(ct);
    }
}

// =========================================================================
// Shared CodeGen helper — Prefix-keyed WorkflowAppCodeSequences (SERIALIZABLE tx).
// Mirror SubmitProposalHandler.GenerateMaDeXuatAsync. Format: {prefix}/{seq:D3}.
//   Travel  prefix = "DT/CT/{year}"
//   Vehicle prefix = "DX/XE/{year}"
// =========================================================================

internal static class TravelVehicleCodeGen
{
    internal static async Task<string> GenerateMaDonTuAsync(
        IApplicationDbContext db, string prefix, IDateTime clock, CancellationToken ct)
    {
        var dbContext = (DbContext)db;
        await using var tx = await dbContext.Database.BeginTransactionAsync(IsolationLevel.Serializable, ct);
        var seq = await db.WorkflowAppCodeSequences.FirstOrDefaultAsync(s => s.Prefix == prefix, ct);
        if (seq is null)
        {
            seq = new WorkflowAppCodeSequence { Prefix = prefix, LastSeq = 0, UpdatedAt = clock.UtcNow };
            db.WorkflowAppCodeSequences.Add(seq);
        }
        seq.LastSeq++;
        seq.UpdatedAt = clock.UtcNow;
        await db.SaveChangesAsync(ct);
        await tx.CommitAsync(ct);
        return $"{prefix}/{seq.LastSeq:D3}";
    }
}
