using FluentValidation;
using MediatR;
using Microsoft.EntityFrameworkCore;
using SolutionErp.Application.Common.Exceptions;
using SolutionErp.Application.Common.Interfaces;
using SolutionErp.Application.Common.Models;
using SolutionErp.Application.PurchaseEvaluations.Dtos;
using SolutionErp.Domain.Contracts;              // ChangelogAction (dùng chung PE)
using SolutionErp.Domain.Identity;               // AppRoles
using SolutionErp.Domain.PurchaseEvaluations;

namespace SolutionErp.Application.PurchaseEvaluations;

// =====================================================================================
// [S155 đợt 2] XÓA MỀM phiếu khi ĐANG DUYỆT + màn "Đã xóa".
//
// Vì sao có file này (UAT prod Tra Sol 2026-07-27): phiếu bấm SAI gói thầu đang treo ở
// `ChoDuyet` vẫn **ăn lũy kế** ngân sách hạng mục — `PrevSubmitted` gom `ChoDuyet||DaDuyet`
// (`PeBudgetAccumulator.cs:53-59`) và `PendingSelected` gom ChoDuyet-có-winner (`:114-126`).
// "Trả lại" chỉ DỜI số sang dòng khác chứ không triệt tiêu, còn đường xóa cũ chỉ nhận
// {DangSoanThao, TuChoi} (`PurchaseEvaluationFeatures.cs:1404-1406`) ⇒ không cửa nào dừng.
// Xóa mềm đặt `IsDeleted=true` ⇒ global filter `PurchaseEvaluationConfiguration.cs:84`
// tự loại phiếu khỏi CẢ 4 phép cộng lũy kế — KHÔNG phải sửa accumulator.
//
// 🔴 TÁCH ĐƯỜNG (owner chốt sau review H1): đường xóa NHÁP cũ
// `DELETE /api/purchase-evaluations/{id}` GIỮ NGUYÊN **không policy** — gắn policy vào đó
// làm role `Drafter` mất quyền xóa nháp của chính mình (đo thật trên DB Dev: 11/13 role
// `CanDelete=0`, `DbInitializer.cs:2515` chỉ cấp cho DeptManager). Đường MỚI ở đây là
// endpoint RIÊNG mang `[Authorize(Policy = "PurchaseEvaluations.Delete")]`.
// =====================================================================================

// ========== A. DELETE BY APPROVER (phiếu đang duyệt) ==========

/// <param name="Reason">Lý do xóa — optional. `DeletedBy`/`DeletedAt` do `AuditingInterceptor`
/// set chỉ trả lời "ai + lúc nào", KHÔNG có LÝ DO ⇒ changelog là chỗ chứa lý do.</param>
public record DeletePurchaseEvaluationByApproverCommand(Guid Id, string? Reason = null) : IRequest;

public class DeletePurchaseEvaluationByApproverCommandValidator
    : AbstractValidator<DeletePurchaseEvaluationByApproverCommand>
{
    public DeletePurchaseEvaluationByApproverCommandValidator()
    {
        RuleFor(x => x.Id).NotEmpty();
        // MaxLength MATCH EF config: `PurchaseEvaluationChangelog.ContextNote` HasMaxLength(2000)
        // (`PurchaseEvaluationConfiguration.cs:184`) — EF là source of truth, không trust spec.
        RuleFor(x => x.Reason).MaximumLength(2000);
    }
}

public class DeletePurchaseEvaluationByApproverCommandHandler(
    IApplicationDbContext db,
    ICurrentUser currentUser) : IRequestHandler<DeletePurchaseEvaluationByApproverCommand>
{
    public async Task Handle(DeletePurchaseEvaluationByApproverCommand request, CancellationToken ct)
    {
        if (!currentUser.IsAuthenticated || currentUser.UserId is null)
            throw new UnauthorizedException();
        var actorId = currentUser.UserId.Value;

        var pe = await db.PurchaseEvaluations.FirstOrDefaultAsync(x => x.Id == request.Id, ct)
            ?? throw new NotFoundException("PurchaseEvaluation", request.Id);

        // ---- Rào (i): PHASE. Đường này CHỈ cho phiếu đang treo ở bước duyệt.
        // Phiếu nháp đi đường cũ (7a) — cố tình KHÔNG gộp để không đụng luồng đang sống.
        if (pe.Phase != PurchaseEvaluationPhase.ChoDuyet)
            throw new ConflictException(
                "Đường xóa này chỉ dùng cho phiếu đang ở trạng thái Đã gửi duyệt. " +
                "Phiếu nháp dùng nút Xóa ở màn soạn thảo.");

        if (pe.ApprovalWorkflowId is not Guid awId)
            throw new ConflictException(
                "Phiếu chưa gắn quy trình duyệt (V2) nên không xác định được lượt duyệt.");

        // ---- Rào (ii): ĐÚNG LƯỢT. Khuôn `ApproveV2Async`
        // (`PurchaseEvaluationWorkflowService.cs:677-720`): `CurrentWorkflowStepIndex` là
        // INDEX vào list Steps đã sort theo Order; `CurrentApprovalLevelOrder` so GIÁ TRỊ Order.
        var steps = await db.ApprovalWorkflowSteps.AsNoTracking()
            .Where(s => s.ApprovalWorkflowId == awId)
            .OrderBy(s => s.Order)
            .Select(s => new { s.Id, s.Order, s.Name })
            .ToListAsync(ct);
        if (steps.Count == 0)
            throw new ConflictException("Quy trình chưa có bước nào.");

        var currentIdx = pe.CurrentWorkflowStepIndex ?? 0;
        if (currentIdx < 0 || currentIdx >= steps.Count)
            throw new ConflictException(
                $"CurrentWorkflowStepIndex={currentIdx} không hợp lệ (max={steps.Count - 1}).");
        var currentStep = steps[currentIdx];
        var currentLevelOrder = pe.CurrentApprovalLevelOrder ?? 1;

        var levelsOfStep = await db.ApprovalWorkflowLevels.AsNoTracking()
            .Where(l => l.ApprovalWorkflowStepId == currentStep.Id)
            .OrderBy(l => l.Order)
            .ToListAsync(ct);
        var pendingLevelGroup = levelsOfStep.Where(l => l.Order == currentLevelOrder).ToList();
        if (pendingLevelGroup.Count == 0)
            throw new ConflictException($"Bước {currentIdx + 1} không có cấp {currentLevelOrder}.");

        // 🔴 PER-ROW, KHÔNG fallback `.First()`. Fallback `pendingLevelGroup.First()` ở
        // `PurchaseEvaluationWorkflowService.cs:744-745` là đường Admin-KÝ-THAY khi DUYỆT —
        // bê sang đây sẽ biến "admin bất kỳ" thành người xóa được phiếu người khác.
        // Không match ⇒ chưa tới lượt ⇒ 403 (test 24).
        var matchingLevel = pendingLevelGroup.FirstOrDefault(l => l.ApproverUserId == actorId)
            ?? throw new ForbiddenException(
                $"Bước {currentIdx + 1} ({currentStep.Name}) — Cấp {currentLevelOrder}: " +
                "chưa tới lượt duyệt của bạn nên không xóa được phiếu này.");

        // ---- Rào (iii): CỜ F6 **PER-ROW** (owner chốt per-NGƯỜI sau review H2).
        // Đọc `matchingLevel.AllowApproverDelete` (khuôn F5 `PurchaseEvaluationWorkflowService.cs:859`),
        // KHÔNG đọc per-Cấp `g.Any(l => ...)` (`PurchaseEvaluationFeatures.cs:1182`): 1 Cấp có N
        // người (OR-of-N — ảnh UAT: Cấp 2 Phòng Cung ứng 3 người) ⇒ `Any` phát quyền huỷ
        // chứng-từ tài chính cho CẢ Cấp thay vì đúng 1 người admin chỉ đích danh.
        if (!matchingLevel.AllowApproverDelete)
            throw new ForbiddenException(
                $"Cấp duyệt của bạn (Bước {currentIdx + 1} — Cấp {currentLevelOrder}) chưa được bật " +
                "quyền \"Xóa phiếu khi đang duyệt\". Liên hệ quản trị để bật trong Quy trình duyệt.");

        // ---- Ghi VẾT trước khi xóa (khuôn `LogTransitionAsync`
        // `PurchaseEvaluationWorkflowService.cs:1169-1196`). `ChangelogAction.Delete = 3` đã tồn
        // tại (`ContractChangelog.cs:38-44`) ⇒ KHÔNG cần migration.
        // `PurchaseEvaluationChangelog : BaseEntity` (không `IsDeleted`) + query changelog lọc
        // theo `PurchaseEvaluationId` KHÔNG join PE (`PurchaseEvaluationFeatures.cs:1424-1425`)
        // ⇒ vết vẫn đọc được sau khi phiếu biến mất khỏi global filter.
        var reason = string.IsNullOrWhiteSpace(request.Reason) ? null : request.Reason.Trim();

        // 🔴 THỨ TỰ QUAN TRỌNG — `Remove()` TRƯỚC, `Add(changelog)` SAU. Đảo lại là MẤT VẾT.
        // Bug T26 bắt được (S155 đợt 2): EF chạy cascade client-side NGAY tại lời gọi
        // `Remove()` (`CascadeDeleteTiming.Immediate` = mặc định). Nếu changelog đã ở state
        // `Added` trước đó, EF thấy cha "sắp bị xóa" nên DETACH đứa con vừa thêm ⇒ SaveChanges
        // ghi 0 row, im lặng, không lỗi. Đo thật: `stateAfterAdd=Added → stateAfterRemove=Detached
        // → rowsInDb=0`.
        // Trớ trêu: phiếu KHÔNG hề bị xóa thật — `AuditingInterceptor` đổi Deleted→Modified
        // thành xóa MỀM. Nhưng cascade chạy TRƯỚC interceptor nên nó hành động dựa trên một vụ
        // xóa sẽ không bao giờ xảy ra.
        //
        // 🔴 CƠ CHẾ CỨU — đọc kỹ, đừng suy sai rồi dời `Add()` lên trên:
        // KHÔNG phải "thêm sau khi state đã đổi". Tại thời điểm `Add()` chạy, phiếu **VẪN ĐANG
        // là `Deleted`** — interceptor mãi tới `SaveChanges` mới đổi sang `Modified`. Thứ thật
        // sự cứu bản ghi là: **lượt cascade tức thời ĐÃ XẢY RA XONG ngay tại `Remove()`, lúc
        // chưa có đứa con nào để gỡ**. Không có lượt quét thứ hai ở `SaveChanges` gỡ lại
        // (T26 đọc NGƯỢC từ DB sau khi lưu và đòi đúng 2 dòng — nếu còn lượt quét thì phải là 0).
        // Chọn cách này thay vì `CascadeDeleteTiming.Never` (đụng setting toàn context) hoặc
        // 2 lần SaveChanges (đẻ nhu cầu transaction) — rẻ nhất và không có tác dụng phụ.
        db.PurchaseEvaluations.Remove(pe);

        db.PurchaseEvaluationChangelogs.Add(new PurchaseEvaluationChangelog
        {
            PurchaseEvaluationId = pe.Id,
            EntityType = PurchaseEvaluationEntityType.Workflow,
            EntityId = matchingLevel.Id,
            Action = ChangelogAction.Delete,
            PhaseAtChange = pe.Phase,                  // ChoDuyet — phase TẠI LÚC xóa
            UserId = actorId,
            UserName = currentUser.FullName ?? currentUser.Email,
            Summary = $"Xóa phiếu khi đang duyệt (Bước {currentIdx + 1} — Cấp {currentLevelOrder})",
            ContextNote = reason,
        });

        // Nhắc lại cho người đọc sau: `.Remove()` ở TRÊN = XÓA MỀM, không phải hard-delete.
        // `AuditingInterceptor.cs:54-63` đổi state Deleted→Modified rồi set
        // IsDeleted/DeletedAt/DeletedBy (indirect-assignment). Row PE ở lại ⇒ FK của
        // changelog vừa thêm vẫn hợp lệ.
        await db.SaveChangesAsync(ct);
    }
}

// ========== B. LIST "ĐÃ XÓA" ==========

/// Màn "Đã xóa" — CHỈ XEM (owner chốt (6): chưa làm khôi phục).
public record ListDeletedPurchaseEvaluationsQuery(PurchaseEvaluationType? Type = null)
    : PagedRequest, IRequest<PagedResult<PurchaseEvaluationListItemDto>>;

public class ListDeletedPurchaseEvaluationsQueryHandler(
    IApplicationDbContext db,
    ICurrentUser currentUser)
    : IRequestHandler<ListDeletedPurchaseEvaluationsQuery, PagedResult<PurchaseEvaluationListItemDto>>
{
    public async Task<PagedResult<PurchaseEvaluationListItemDto>> Handle(
        ListDeletedPurchaseEvaluationsQuery request, CancellationToken ct)
    {
        // 🔴 8a — `IgnoreQueryFilters()` **GỠ** filter chứ KHÔNG ĐẢO nó. Thiếu
        // `.Where(x => x.IsDeleted)` ⇒ màn "Đã xóa" liệt kê TOÀN BỘ phiếu sống lẫn chết.
        // 🔴 D3 (S159-đợt5, sửa lần 2 theo reviewer FLAG-10 — BỎ CON SỐ ĐẾM, nêu LUẬT):
        // MỌI nơi dùng `IgnoreQueryFilters` để LIỆT KÊ bản ghi đã xóa BẮT BUỘC kèm
        // `.Where(x => x.IsDeleted)`. Lớp list-deleted hiện: đây + `ListDeletedContractsQueryHandler`
        // (ContractFeatures.cs). `ApprovalWorkflowV2AdminFeatures.cs` cũng dùng Ignore ở nhiều
        // vị trí nhưng thuộc LỚP KHÁC (đọc *LevelOpinions + dò phiếu-cha-mồ-côi, không list-deleted).
        // Tự kiểm: grep -rn "IgnoreQueryFilters" src/Backend --include=*.cs
        // (Bài: "chỗ DUY NHẤT/THỨ HAI" là con số tự-lan — S155 ghi DUY NHẤT đã sai từ khi
        //  ApprovalWorkflowV2AdminFeatures dùng; đừng thay số sai cũ bằng số sai mới.)
        // CẤM đưa vào helper dùng chung trả `IQueryable`, CẤM sửa
        // `PurchaseEvaluationConfiguration.cs:84`, CẤM thêm cờ `includeDeleted` vào query list
        // hiện có (cờ sẽ lan tới `PeBudgetAccumulator` = phá đúng thứ owner muốn).
        var q = from e in db.PurchaseEvaluations.AsNoTracking().IgnoreQueryFilters()
                    .Where(x => x.IsDeleted)
                join p in db.Projects.AsNoTracking() on e.ProjectId equals p.Id
                join s in db.Suppliers.AsNoTracking() on e.SelectedSupplierId equals s.Id into sj
                from s in sj.DefaultIfEmpty()
                join u in db.Users.AsNoTracking() on e.DrafterUserId equals u.Id into uj
                from u in uj.DefaultIfEmpty()
                join d in db.Departments.AsNoTracking() on e.DepartmentId equals d.Id into dj
                from d in dj.DefaultIfEmpty()
                join wi in db.WorkItems.AsNoTracking() on e.WorkItemId equals wi.Id into wij
                from wi in wij.DefaultIfEmpty()
                select new { e, p, s, u, d, wi };

        // 🔴 8b — handler MỚI mặc định KHÔNG có rào nào. Tái lập IDOR-scope y hệt list sống
        // (`PurchaseEvaluationFeatures.cs:596-617`): non-admin chỉ thấy phiếu khi là Drafter
        // ∥ role eligible theo phase ∥ là approver của workflow đã pin. Thiếu bước này = mọi
        // user đọc được toàn bộ phiếu đã xóa của mọi dự án kèm `BudgetPeriodAmount` + tên gói
        // thầu + NCC.
        if (!currentUser.Roles.Contains(AppRoles.Admin))
        {
            var userId = currentUser.UserId;
            var eligiblePhases = ListPurchaseEvaluationsQueryHandler.GetEligiblePhases(currentUser.Roles);
            var userApprovalWfIds = userId is null
                ? new List<Guid>()
                : await db.ApprovalWorkflowLevels.AsNoTracking()
                    .Where(l => l.ApproverUserId == userId.Value)
                    .Select(l => l.Step!.ApprovalWorkflowId)
                    .Distinct()
                    .ToListAsync(ct);
            q = q.Where(x =>
                x.e.DrafterUserId == userId
                || eligiblePhases.Contains(x.e.Phase)
                || (x.e.Phase != PurchaseEvaluationPhase.DangSoanThao
                    && x.e.ApprovalWorkflowId != null
                    && userApprovalWfIds.Contains(x.e.ApprovalWorkflowId.Value)));
        }

        if (request.Type is not null) q = q.Where(x => x.e.Type == request.Type);

        if (!string.IsNullOrWhiteSpace(request.Search))
        {
            var s = request.Search.Trim();
            q = q.Where(x =>
                (x.e.MaPhieu != null && x.e.MaPhieu.Contains(s)) ||
                x.e.TenGoiThau.Contains(s) ||
                x.p.Name.Contains(s) ||
                x.p.Code.Contains(s));
        }

        // Mới xóa lên đầu: `AuditingInterceptor` set `UpdatedAt` ngay tại lượt SaveChanges
        // của thao tác xóa mềm ⇒ UpdatedAt ≈ DeletedAt (DTO dùng chung không có cột DeletedAt).
        q = request.SortDesc
            ? q.OrderByDescending(x => x.e.UpdatedAt ?? x.e.CreatedAt)
            : q.OrderBy(x => x.e.UpdatedAt ?? x.e.CreatedAt);

        var total = await q.CountAsync(ct);
        var items = await q
            .Skip((request.Page - 1) * request.PageSize).Take(request.PageSize)
            .Select(x => new PurchaseEvaluationListItemDto(
                x.e.Id, x.e.MaPhieu, x.e.TenGoiThau, x.e.Type, x.e.Phase,
                x.e.ProjectId, x.p.Name, x.p.Code,
                x.e.WorkItemId, x.wi != null ? x.wi.Name : null, x.wi != null ? x.wi.Code : null,
                x.e.SelectedSupplierId, x.s != null ? x.s.Name : null,
                x.e.ContractId, x.e.SlaDeadline, x.e.CreatedAt, x.e.UpdatedAt,
                x.e.DrafterUserId, x.u != null ? x.u.FullName : null,
                x.e.DepartmentId, x.d != null ? x.d.Name : null,
                x.e.BudgetPeriodAmount, x.e.ExpectedRemainingAmount,
                x.e.IsUrgentByPro, x.e.IsUrgentByCcm,
                (from sw in db.PurchaseEvaluationSuppliers
                 where sw.PurchaseEvaluationId == x.e.Id && sw.IsWinner
                 join sup in db.Suppliers on sw.SupplierId equals sup.Id
                 orderby sup.Name
                 select sup.Name).ToList(),
                // Badge "Kết thúc trước CEO" = thông tin về ĐƯỜNG ĐI CÒN LẠI của phiếu.
                // Phiếu đã xóa không còn đường đi ⇒ trả false/null tường minh thay vì tính
                // lại con-trỏ (khai rõ để người sau không tưởng là bug quên map).
                false, null, null))
            .ToListAsync(ct);

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