using FluentValidation;
using MediatR;
using Microsoft.EntityFrameworkCore;
using SolutionErp.Application.Common.Exceptions;
using SolutionErp.Application.Common.Interfaces;
using SolutionErp.Domain.Contracts;
using SolutionErp.Domain.Identity;
using SolutionErp.Domain.PurchaseEvaluations;

namespace SolutionErp.Application.PurchaseEvaluations;

// ========== Helper: Lock edit guard (Phase 9 — Migration 16) ==========
// Original: Chỉ Phase=DangSoanThao mới được CRUD chi tiết / báo giá / NCC tham gia.
// Đã trình duyệt → KHÔNG sửa được. Phải reject về DangSoanThao trước.
//
// Mig 28 (S21 t4 — F3): Extend Section 2 (Detail + NCC + Báo giá) cho phép
// Approver edit khi phase=ChoDuyet + workflow.AllowApproverEditDetails=true +
// actor.Id == currentLevel.ApproverUserId. KHÔNG đụng PE Header / Attachment /
// DepartmentOpinion — vẫn dùng EnsureDraftAsync strict.
internal static class PurchaseEvaluationDraftGuard
{
    /// Strict guard cho PE Header / Attachment / DepartmentOpinion / Winner select —
    /// chỉ Drafter scope (DangSoanThao OR TraLai để Drafter sửa rồi gửi lại).
    public static async Task<PurchaseEvaluation> EnsureDraftAsync(IApplicationDbContext db, Guid id, CancellationToken ct)
    {
        var pe = await db.PurchaseEvaluations.FirstOrDefaultAsync(x => x.Id == id, ct)
            ?? throw new NotFoundException("PurchaseEvaluation", id);
        if (pe.Phase != PurchaseEvaluationPhase.DangSoanThao
            && pe.Phase != PurchaseEvaluationPhase.TraLai)
            throw new ConflictException(
                $"Phiếu PE đã trình duyệt (Phase={pe.Phase}), không thể chỉnh sửa. " +
                "Phải Trả lại Drafter sửa lại.");
        return pe;
    }

    /// F3 (Mig 28 — S21 t4) — Edit guard cho Section 2 (Detail + NCC + Báo giá).
    /// 2 trường hợp accepted:
    ///   1. Drafter scope: DangSoanThao OR TraLai — Controller [Authorize] handle role.
    ///   2. Approver scope: ChoDuyet + workflow.AllowApproverEditDetails=true +
    ///      actor.Id match CurrentLevel.ApproverUserId. KHÔNG reset workflow,
    ///      giữ Cấp hiện tại. Admin bypass workflow flag check.
    public static async Task<PurchaseEvaluation> EnsureEditableForDetailsAsync(
        IApplicationDbContext db, Guid id, ICurrentUser currentUser, CancellationToken ct)
    {
        var pe = await db.PurchaseEvaluations.FirstOrDefaultAsync(x => x.Id == id, ct)
            ?? throw new NotFoundException("PurchaseEvaluation", id);

        // Drafter scope — any authenticated, Controller [Authorize(Policy)] gates role
        if (pe.Phase == PurchaseEvaluationPhase.DangSoanThao
            || pe.Phase == PurchaseEvaluationPhase.TraLai)
            return pe;

        // F3 Approver scope (Mig 28) — chỉ ChoDuyet với V2 schema
        if (pe.Phase == PurchaseEvaluationPhase.ChoDuyet
            && currentUser.IsAuthenticated
            && currentUser.UserId is Guid actorUserId)
        {
            // Admin bypass — admin có thể edit bất chấp Allow* flag
            if (currentUser.Roles.Contains(AppRoles.Admin)) return pe;

            // V2 schema required
            if (pe.ApprovalWorkflowId is Guid awId
                && pe.CurrentWorkflowStepIndex is int stepIdx
                && pe.CurrentApprovalLevelOrder is int levelOrder)
            {
                var workflow = await db.ApprovalWorkflows
                    .Include(w => w.Steps).ThenInclude(s => s.Levels)
                    .FirstOrDefaultAsync(w => w.Id == awId, ct)
                    ?? throw new ConflictException("Workflow không tồn tại.");

                var step = workflow.Steps.OrderBy(s => s.Order).Skip(stepIdx).FirstOrDefault();
                // Plan O S23 t5 — Per-NV lookup site MUST discriminate ApproverUserId.
                // Schema Mig 29 OR-of-N: N rows cùng Order. Filter actor match
                // ngay trong FirstOrDefault tránh chọn nhầm row đầu DB.
                var level = step?.Levels.FirstOrDefault(lv =>
                    lv.Order == levelOrder && lv.ApproverUserId == actorUserId);
                if (level is null)
                    throw new ForbiddenException(
                        $"Chỉ NV phụ trách Bước {step?.Order} / Cấp {levelOrder} " +
                        "mới được chỉnh sửa Section 2 lúc đang duyệt.");

                // Mig 29 (S21 t5) — F3 flag move xuống Level slot (per-NV). Đọc
                // từ level.AllowApproverEditDetails của chính actor.
                if (!level.AllowApproverEditDetails)
                    throw new ConflictException(
                        $"Cấp Approver hiện tại (Bước {step!.Order} / Cấp {levelOrder}) " +
                        "không được cấp quyền chỉnh sửa Section 2. " +
                        "Phải Trả lại Drafter sửa hoặc liên hệ Admin Designer cấp quyền slot.");

                return pe;
            }

            throw new ConflictException(
                "Phiếu chưa pin workflow V2 hoặc chưa init Bước/Cấp — không thể chỉnh sửa.");
        }

        throw new ConflictException(
            $"Phiếu PE ở Phase={pe.Phase}, không thể chỉnh sửa Section 2. " +
            "Phải Trả lại Drafter sửa.");
    }
}

// ========== Detail (hạng mục + ngân sách) ==========

public record AddPurchaseEvaluationDetailCommand(
    Guid PurchaseEvaluationId,
    string GroupCode,
    string GroupName,
    string? ItemCode,
    string NoiDung,
    string? DonViTinh,
    decimal KhoiLuongNganSach,
    decimal KhoiLuongThiCong,
    decimal DonGiaNganSach,
    decimal ThanhTienNganSach,
    string? GhiChu) : IRequest<Guid>;

public class AddPurchaseEvaluationDetailCommandValidator : AbstractValidator<AddPurchaseEvaluationDetailCommand>
{
    public AddPurchaseEvaluationDetailCommandValidator()
    {
        RuleFor(x => x.PurchaseEvaluationId).NotEmpty();
        RuleFor(x => x.GroupCode).NotEmpty().MaximumLength(50);
        RuleFor(x => x.GroupName).NotEmpty().MaximumLength(200);
        RuleFor(x => x.NoiDung).NotEmpty().MaximumLength(500);
    }
}

public class AddPurchaseEvaluationDetailCommandHandler(
    IApplicationDbContext db,
    ICurrentUser currentUser) : IRequestHandler<AddPurchaseEvaluationDetailCommand, Guid>
{
    public async Task<Guid> Handle(AddPurchaseEvaluationDetailCommand request, CancellationToken ct)
    {
        var evaluation = await PurchaseEvaluationDraftGuard.EnsureEditableForDetailsAsync(
            db, request.PurchaseEvaluationId, currentUser, ct);

        var maxOrder = await db.PurchaseEvaluationDetails
            .Where(d => d.PurchaseEvaluationId == request.PurchaseEvaluationId)
            .Select(d => (int?)d.Order)
            .MaxAsync(ct);

        var entity = new PurchaseEvaluationDetail
        {
            PurchaseEvaluationId = request.PurchaseEvaluationId,
            GroupCode = request.GroupCode,
            GroupName = request.GroupName,
            ItemCode = request.ItemCode,
            NoiDung = request.NoiDung,
            DonViTinh = request.DonViTinh,
            KhoiLuongNganSach = request.KhoiLuongNganSach,
            KhoiLuongThiCong = request.KhoiLuongThiCong,
            DonGiaNganSach = request.DonGiaNganSach,
            ThanhTienNganSach = request.ThanhTienNganSach,
            GhiChu = request.GhiChu,
            Order = (maxOrder ?? 0) + 1,
        };
        db.PurchaseEvaluationDetails.Add(entity);

        db.PurchaseEvaluationChangelogs.Add(new PurchaseEvaluationChangelog
        {
            PurchaseEvaluationId = request.PurchaseEvaluationId,
            EntityType = PurchaseEvaluationEntityType.Detail,
            EntityId = entity.Id,
            Action = ChangelogAction.Insert,
            PhaseAtChange = evaluation.Phase,
            UserId = currentUser.UserId,
            UserName = currentUser.FullName ?? currentUser.Email,
            Summary = $"Thêm hạng mục {request.GroupCode} — {request.NoiDung}",
        });

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

public record UpdatePurchaseEvaluationDetailCommand(
    Guid PurchaseEvaluationId,
    Guid DetailId,
    string GroupCode,
    string GroupName,
    string? ItemCode,
    string NoiDung,
    string? DonViTinh,
    decimal KhoiLuongNganSach,
    decimal KhoiLuongThiCong,
    decimal DonGiaNganSach,
    decimal ThanhTienNganSach,
    string? GhiChu) : IRequest;

public class UpdatePurchaseEvaluationDetailCommandHandler(
    IApplicationDbContext db,
    ICurrentUser currentUser) : IRequestHandler<UpdatePurchaseEvaluationDetailCommand>
{
    public async Task Handle(UpdatePurchaseEvaluationDetailCommand request, CancellationToken ct)
    {
        var evaluation = await PurchaseEvaluationDraftGuard.EnsureEditableForDetailsAsync(
            db, request.PurchaseEvaluationId, currentUser, ct);
        var entity = await db.PurchaseEvaluationDetails
            .FirstOrDefaultAsync(x => x.Id == request.DetailId && x.PurchaseEvaluationId == request.PurchaseEvaluationId, ct)
            ?? throw new NotFoundException("PurchaseEvaluationDetail", request.DetailId);

        entity.GroupCode = request.GroupCode;
        entity.GroupName = request.GroupName;
        entity.ItemCode = request.ItemCode;
        entity.NoiDung = request.NoiDung;
        entity.DonViTinh = request.DonViTinh;
        entity.KhoiLuongNganSach = request.KhoiLuongNganSach;
        entity.KhoiLuongThiCong = request.KhoiLuongThiCong;
        entity.DonGiaNganSach = request.DonGiaNganSach;
        entity.ThanhTienNganSach = request.ThanhTienNganSach;
        entity.GhiChu = request.GhiChu;

        // F3 audit (Mig 28) — log Approver edit Section 2. Drafter edit cũng log
        // để audit trail consistent. Phase ChoDuyet → flag "Approver" trong summary.
        var approverNote = evaluation.Phase == PurchaseEvaluationPhase.ChoDuyet
            ? " [Approver edit khi đang duyệt]" : string.Empty;
        db.PurchaseEvaluationChangelogs.Add(new PurchaseEvaluationChangelog
        {
            PurchaseEvaluationId = request.PurchaseEvaluationId,
            EntityType = PurchaseEvaluationEntityType.Detail,
            EntityId = entity.Id,
            Action = ChangelogAction.Update,
            PhaseAtChange = evaluation.Phase,
            UserId = currentUser.UserId,
            UserName = currentUser.FullName ?? currentUser.Email,
            Summary = $"Cập nhật hạng mục {request.GroupCode} — {request.NoiDung}{approverNote}",
        });

        await db.SaveChangesAsync(ct);
    }
}

public record DeletePurchaseEvaluationDetailCommand(Guid PurchaseEvaluationId, Guid DetailId) : IRequest;

public class DeletePurchaseEvaluationDetailCommandHandler(
    IApplicationDbContext db,
    ICurrentUser currentUser) : IRequestHandler<DeletePurchaseEvaluationDetailCommand>
{
    public async Task Handle(DeletePurchaseEvaluationDetailCommand request, CancellationToken ct)
    {
        var evaluation = await PurchaseEvaluationDraftGuard.EnsureEditableForDetailsAsync(
            db, request.PurchaseEvaluationId, currentUser, ct);
        var entity = await db.PurchaseEvaluationDetails
            .FirstOrDefaultAsync(x => x.Id == request.DetailId && x.PurchaseEvaluationId == request.PurchaseEvaluationId, ct)
            ?? throw new NotFoundException("PurchaseEvaluationDetail", request.DetailId);

        var approverNote = evaluation.Phase == PurchaseEvaluationPhase.ChoDuyet
            ? " [Approver edit khi đang duyệt]" : string.Empty;
        db.PurchaseEvaluationChangelogs.Add(new PurchaseEvaluationChangelog
        {
            PurchaseEvaluationId = request.PurchaseEvaluationId,
            EntityType = PurchaseEvaluationEntityType.Detail,
            EntityId = entity.Id,
            Action = ChangelogAction.Delete,
            PhaseAtChange = evaluation.Phase,
            UserId = currentUser.UserId,
            UserName = currentUser.FullName ?? currentUser.Email,
            Summary = $"Xóa hạng mục {entity.GroupCode} — {entity.NoiDung}{approverNote}",
        });

        db.PurchaseEvaluationDetails.Remove(entity);
        await db.SaveChangesAsync(ct);
        // [S114 A2 INVARIANT — Bước 4 verify bắt] Cascade xóa quotes của hạng mục có thể bỏ quote
        // IsSelected cuối của 1 NCC → re-derive IsWinner toàn phiếu (writer bị sót lúc implement).
        await PeWinnerInvariant.ReSyncAfterQuoteRemovalAsync(db, request.PurchaseEvaluationId, ct);
    }
}

// ========== Quote (báo giá per NCC per Detail) — upsert ==========

public record UpsertPurchaseEvaluationQuoteCommand(
    Guid PurchaseEvaluationId,
    Guid PurchaseEvaluationDetailId,
    Guid PurchaseEvaluationSupplierId,
    decimal BgVat,
    decimal ChuaVat,
    decimal ThanhTien,
    bool IsSelected,
    string? Note) : IRequest<Guid>;

public class UpsertPurchaseEvaluationQuoteCommandHandler(
    IApplicationDbContext db,
    ICurrentUser currentUser) : IRequestHandler<UpsertPurchaseEvaluationQuoteCommand, Guid>
{
    public async Task<Guid> Handle(UpsertPurchaseEvaluationQuoteCommand request, CancellationToken ct)
    {
        var evaluation = await PurchaseEvaluationDraftGuard.EnsureEditableForDetailsAsync(
            db, request.PurchaseEvaluationId, currentUser, ct);
        // Verify parents exist + same phiếu
        var detail = await db.PurchaseEvaluationDetails.FirstOrDefaultAsync(
            d => d.Id == request.PurchaseEvaluationDetailId && d.PurchaseEvaluationId == request.PurchaseEvaluationId, ct)
            ?? throw new NotFoundException("PurchaseEvaluationDetail", request.PurchaseEvaluationDetailId);

        var supplier = await db.PurchaseEvaluationSuppliers.FirstOrDefaultAsync(
            s => s.Id == request.PurchaseEvaluationSupplierId && s.PurchaseEvaluationId == request.PurchaseEvaluationId, ct)
            ?? throw new NotFoundException("PurchaseEvaluationSupplier", request.PurchaseEvaluationSupplierId);

        var existing = await db.PurchaseEvaluationQuotes.FirstOrDefaultAsync(
            q => q.PurchaseEvaluationDetailId == request.PurchaseEvaluationDetailId
                && q.PurchaseEvaluationSupplierId == request.PurchaseEvaluationSupplierId, ct);

        var approverNote = evaluation.Phase == PurchaseEvaluationPhase.ChoDuyet
            ? " [Approver edit khi đang duyệt]" : string.Empty;

        if (existing is not null)
        {
            existing.BgVat = request.BgVat;
            existing.ChuaVat = request.ChuaVat;
            existing.ThanhTien = request.ThanhTien;
            existing.IsSelected = request.IsSelected;
            existing.Note = request.Note;
            // A2(b) INVARIANT: supplier.IsWinner == đơn vị có ÍT NHẤT 1 quote IsSelected. Đổi
            // IsSelected của 1 quote → re-derive IsWinner của supplier-row (quote KHÁC từ DB +
            // OR giá trị vừa set — DB chưa flush thay đổi hiện tại nên loại trừ existing.Id).
            supplier.IsWinner = request.IsSelected
                || await db.PurchaseEvaluationQuotes.AsNoTracking().AnyAsync(
                    q => q.PurchaseEvaluationSupplierId == request.PurchaseEvaluationSupplierId
                        && q.Id != existing.Id && q.IsSelected, ct);
            db.PurchaseEvaluationChangelogs.Add(new PurchaseEvaluationChangelog
            {
                PurchaseEvaluationId = request.PurchaseEvaluationId,
                EntityType = PurchaseEvaluationEntityType.Quote,
                EntityId = existing.Id,
                Action = ChangelogAction.Update,
                PhaseAtChange = evaluation.Phase,
                UserId = currentUser.UserId,
                UserName = currentUser.FullName ?? currentUser.Email,
                Summary = $"Cập nhật báo giá cho hạng mục {detail.GroupCode}{approverNote}",
            });
            await db.SaveChangesAsync(ct);
            return existing.Id;
        }

        var entity = new PurchaseEvaluationQuote
        {
            PurchaseEvaluationDetailId = request.PurchaseEvaluationDetailId,
            PurchaseEvaluationSupplierId = request.PurchaseEvaluationSupplierId,
            BgVat = request.BgVat,
            ChuaVat = request.ChuaVat,
            ThanhTien = request.ThanhTien,
            IsSelected = request.IsSelected,
            Note = request.Note,
        };
        db.PurchaseEvaluationQuotes.Add(entity);
        // A2(b) INVARIANT — quote MỚI: supplier winner nếu quote này IsSelected hoặc đã có quote
        // IsSelected khác (chưa có cặp (detail,supplier) này trước đó nên không cần loại trừ).
        supplier.IsWinner = request.IsSelected
            || await db.PurchaseEvaluationQuotes.AsNoTracking().AnyAsync(
                q => q.PurchaseEvaluationSupplierId == request.PurchaseEvaluationSupplierId
                    && q.IsSelected, ct);
        db.PurchaseEvaluationChangelogs.Add(new PurchaseEvaluationChangelog
        {
            PurchaseEvaluationId = request.PurchaseEvaluationId,
            EntityType = PurchaseEvaluationEntityType.Quote,
            EntityId = entity.Id,
            Action = ChangelogAction.Insert,
            PhaseAtChange = evaluation.Phase,
            UserId = currentUser.UserId,
            UserName = currentUser.FullName ?? currentUser.Email,
            Summary = $"Thêm báo giá cho hạng mục {detail.GroupCode}{approverNote}",
        });
        await db.SaveChangesAsync(ct);
        return entity.Id;
    }
}

public record DeletePurchaseEvaluationQuoteCommand(Guid PurchaseEvaluationId, Guid QuoteId) : IRequest;

public class DeletePurchaseEvaluationQuoteCommandHandler(
    IApplicationDbContext db,
    ICurrentUser currentUser) : IRequestHandler<DeletePurchaseEvaluationQuoteCommand>
{
    public async Task Handle(DeletePurchaseEvaluationQuoteCommand request, CancellationToken ct)
    {
        var evaluation = await PurchaseEvaluationDraftGuard.EnsureEditableForDetailsAsync(
            db, request.PurchaseEvaluationId, currentUser, ct);
        var quote = await (
            from q in db.PurchaseEvaluationQuotes
            join d in db.PurchaseEvaluationDetails on q.PurchaseEvaluationDetailId equals d.Id
            where q.Id == request.QuoteId && d.PurchaseEvaluationId == request.PurchaseEvaluationId
            select q).FirstOrDefaultAsync(ct)
            ?? throw new NotFoundException("PurchaseEvaluationQuote", request.QuoteId);

        var approverNote = evaluation.Phase == PurchaseEvaluationPhase.ChoDuyet
            ? " [Approver edit khi đang duyệt]" : string.Empty;
        db.PurchaseEvaluationChangelogs.Add(new PurchaseEvaluationChangelog
        {
            PurchaseEvaluationId = request.PurchaseEvaluationId,
            EntityType = PurchaseEvaluationEntityType.Quote,
            EntityId = quote.Id,
            Action = ChangelogAction.Delete,
            PhaseAtChange = evaluation.Phase,
            UserId = currentUser.UserId,
            UserName = currentUser.FullName ?? currentUser.Email,
            Summary = $"Xóa báo giá{approverNote}",
        });

        db.PurchaseEvaluationQuotes.Remove(quote);
        await db.SaveChangesAsync(ct);
        // [S114 A2 INVARIANT — Bước 4 verify bắt] Xóa quote có thể bỏ quote IsSelected CUỐI của 1
        // NCC → re-derive IsWinner (writer bị sót ở implement → nếu không, IsWinner kẹt stale →
        // CreateContract gom NCC phantom sinh HĐ giaTri=0).
        await PeWinnerInvariant.ReSyncAfterQuoteRemovalAsync(db, request.PurchaseEvaluationId, ct);
    }
}

// [S114 A2 — cardinality invariant helper] Re-derive Supplier.IsWinner (== có ÍT NHẤT 1 quote
// IsSelected TOÀN PHIẾU) + re-sync SelectedSupplierId. Gọi SAU khi đã flush 1 mutation XÓA quote
// (DeleteQuote / DeleteDetail cascade) — nơi selection biến mất KHÔNG qua SelectWinner/UpsertQuote
// (2 writer bị sót lúc implement). Đọc DB đã-flush → không cần loại-trừ quote-đã-xóa.
internal static class PeWinnerInvariant
{
    public static async Task ReSyncAfterQuoteRemovalAsync(
        IApplicationDbContext db, Guid peId, CancellationToken ct)
    {
        var supplierRows = await db.PurchaseEvaluationSuppliers
            .Where(s => s.PurchaseEvaluationId == peId).ToListAsync(ct);
        if (supplierRows.Count == 0) return;

        // Supplier-row có ÍT NHẤT 1 quote IsSelected (bất kỳ hạng mục nào) = winner.
        var selectedRowIds = (await (
            from q in db.PurchaseEvaluationQuotes.AsNoTracking()
            join d in db.PurchaseEvaluationDetails.AsNoTracking()
                on q.PurchaseEvaluationDetailId equals d.Id
            where d.PurchaseEvaluationId == peId && q.IsSelected
            select q.PurchaseEvaluationSupplierId).ToListAsync(ct)).ToHashSet();

        var changed = false;
        foreach (var r in supplierRows)
        {
            var win = selectedRowIds.Contains(r.Id);
            if (r.IsWinner != win) { r.IsWinner = win; changed = true; }
        }

        // Sync SelectedSupplierId (trục A legacy display): đúng 1 winner toàn phiếu → đơn vị đó; ≥2 hoặc 0 → null.
        var pe = await db.PurchaseEvaluations.FirstOrDefaultAsync(p => p.Id == peId, ct);
        if (pe is not null)
        {
            var winnerSupplierIds = supplierRows.Where(r => r.IsWinner).Select(r => r.SupplierId).ToList();
            var newSel = winnerSupplierIds.Count == 1 ? winnerSupplierIds[0] : (Guid?)null;
            if (pe.SelectedSupplierId != newSel) { pe.SelectedSupplierId = newSel; changed = true; }
        }

        if (changed) await db.SaveChangesAsync(ct);
    }
}

// ========== Select winner PER HẠNG MỤC (Detail × NCC — multi-NCC/1 hạng mục) ==========

// [multi-NCC 2026-07-12 — anh Kiệt LOCKED] Chọn winner theo TỪNG HẠNG MỤC (Detail): mỗi hạng
// mục có thể có ≥2 NCC/TP cùng trúng, mỗi nhà 1 giá riêng (Quote.ThanhTien). NGUỒN-SỰ-THẬT =
// PurchaseEvaluationQuote.IsSelected (per Detail×Supplier). Supplier.IsWinner = DERIVED (đơn vị
// có ÍT NHẤT 1 quote IsSelected ở bất kỳ hạng mục nào). Body {DetailId, SupplierIds} = "hạng mục
// DetailId, các NCC SupplierIds thắng" → set Quote.IsSelected=true cho (DetailId × mỗi
// SupplierId), false cho NCC còn lại của DetailId đó, rồi derive IsWinner toàn phiếu +
// sync SelectedSupplierId (trục A legacy). SupplierIds rỗng [] = bỏ chọn mọi NCC cho hạng mục này.
public record SelectPurchaseEvaluationWinnerCommand(
    Guid PurchaseEvaluationId, Guid DetailId, IReadOnlyList<Guid> SupplierIds) : IRequest;

public class SelectPurchaseEvaluationWinnerCommandHandler(
    IApplicationDbContext db,
    ICurrentUser currentUser) : IRequestHandler<SelectPurchaseEvaluationWinnerCommand>
{
    public async Task Handle(SelectPurchaseEvaluationWinnerCommand request, CancellationToken ct)
    {
        var entity = await db.PurchaseEvaluations.FirstOrDefaultAsync(x => x.Id == request.PurchaseEvaluationId, ct)
            ?? throw new NotFoundException("PurchaseEvaluation", request.PurchaseEvaluationId);

        // Hạng mục phải thuộc phiếu (chặn cross-phiếu / stale).
        var detail = await db.PurchaseEvaluationDetails.FirstOrDefaultAsync(
            d => d.Id == request.DetailId && d.PurchaseEvaluationId == request.PurchaseEvaluationId, ct)
            ?? throw new NotFoundException("PurchaseEvaluationDetail", request.DetailId);

        // Load mọi supplier-row của phiếu (tracking — set IsWinner derived).
        var supplierRows = await db.PurchaseEvaluationSuppliers
            .Where(s => s.PurchaseEvaluationId == request.PurchaseEvaluationId)
            .ToListAsync(ct);

        // Distinct id chọn (FE có thể gửi trùng) + map SupplierId(master) → supplier-row Id.
        var selectedSupplierIds = request.SupplierIds?.Distinct().ToHashSet() ?? new HashSet<Guid>();
        var rowIdBySupplierId = supplierRows.ToDictionary(r => r.SupplierId, r => r.Id);
        foreach (var sid in selectedSupplierIds)
        {
            if (!rowIdBySupplierId.ContainsKey(sid))
                throw new ConflictException("NCC chưa được thêm vào phiếu đánh giá.");
        }
        var winningRowIds = selectedSupplierIds.Select(sid => rowIdBySupplierId[sid]).ToHashSet();

        // Load MỌI quote của phiếu (JOIN qua Detail) — tracking. Cần cả phiếu để derive IsWinner
        // đúng (1 đơn vị có thể còn thắng hạng mục khác dù mất selection ở hạng mục này).
        var allQuotes = await (
            from q in db.PurchaseEvaluationQuotes
            join d in db.PurchaseEvaluationDetails on q.PurchaseEvaluationDetailId equals d.Id
            where d.PurchaseEvaluationId == request.PurchaseEvaluationId
            select q).ToListAsync(ct);

        // Set IsSelected cho quotes CỦA HẠNG MỤC NÀY: true nếu supplier-row thắng, false còn lại.
        foreach (var q in allQuotes.Where(q => q.PurchaseEvaluationDetailId == request.DetailId))
            q.IsSelected = winningRowIds.Contains(q.PurchaseEvaluationSupplierId);

        // Derive Supplier.IsWinner (INVARIANT): winner ⟺ có ÍT NHẤT 1 quote IsSelected toàn phiếu.
        var selectedRowIds = allQuotes.Where(q => q.IsSelected)
            .Select(q => q.PurchaseEvaluationSupplierId).ToHashSet();
        foreach (var r in supplierRows)
            r.IsWinner = selectedRowIds.Contains(r.Id);

        // Sync SelectedSupplierId (trục A legacy display): đúng 1 đơn vị winner toàn phiếu → đơn vị
        // đó; ≥2 hoặc 0 → null. Derive từ tập IsWinner vừa tính (giữ trục A không stale).
        var winnerSupplierIds = supplierRows.Where(r => r.IsWinner).Select(r => r.SupplierId).ToList();
        entity.SelectedSupplierId = winnerSupplierIds.Count == 1 ? winnerSupplierIds[0] : null;

        db.PurchaseEvaluationChangelogs.Add(new PurchaseEvaluationChangelog
        {
            PurchaseEvaluationId = entity.Id,
            EntityType = PurchaseEvaluationEntityType.Header,
            Action = ChangelogAction.Update,
            PhaseAtChange = entity.Phase,
            UserId = currentUser.UserId,
            UserName = currentUser.FullName ?? currentUser.Email,
            Summary = selectedSupplierIds.Count == 0
                ? $"Bỏ chọn NCC/TP trúng thầu cho hạng mục {detail.GroupCode}"
                : $"Chọn {selectedSupplierIds.Count} NCC/TP trúng thầu cho hạng mục {detail.GroupCode}",
        });

        await db.SaveChangesAsync(ct);
    }
}
