using FluentValidation;
using MediatR;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using SolutionErp.Application.Common.Exceptions;
using SolutionErp.Application.Common.Interfaces;
using SolutionErp.Application.Common.Models;
using SolutionErp.Application.Contracts.Dtos;
using SolutionErp.Application.Contracts.Services;
using SolutionErp.Domain.Contracts;
using SolutionErp.Domain.Identity;

namespace SolutionErp.Application.Contracts;

// ========== CREATE draft ==========

public record CreateContractCommand(
    ContractType Type,
    Guid SupplierId,
    Guid ProjectId,
    Guid? DepartmentId,
    Guid? TemplateId,
    decimal GiaTri,
    string? TenHopDong,
    string? NoiDung,
    bool BypassProcurementAndCCM,
    string? DraftData,
    // [S61 Mig 50] BudgetId DROP (module Budget cũ xóa hẳn) — GIỮ BudgetManual*
    // (ngân sách nhập tay HĐ không đổi).
    string? BudgetManualName,
    decimal? BudgetManualAmount,
    // [Plan B S29 2026-05-22 Chunk E1] Drafter pick V2 workflow lúc create —
    // mirror PE pattern Workspace Select dropdown. Nếu null → fallback V1 auto
    // pick activeWfId (7 prod contract giữ behavior). Mutually exclusive với
    // V1: pin V2 + V1 cùng lúc OK schema (cả 2 nullable) — Service ApproveV2Async
    // branch ưu tiên V2 nếu cả 2 set.
    Guid? ApprovalWorkflowId = null) : IRequest<Guid>;

public class CreateContractCommandValidator : AbstractValidator<CreateContractCommand>
{
    public CreateContractCommandValidator()
    {
        RuleFor(x => x.Type).IsInEnum();
        RuleFor(x => x.SupplierId).NotEmpty();
        RuleFor(x => x.ProjectId).NotEmpty();
        RuleFor(x => x.GiaTri).GreaterThanOrEqualTo(0);
        RuleFor(x => x.TenHopDong).MaximumLength(500);
        RuleFor(x => x.NoiDung).MaximumLength(2000);
        RuleFor(x => x.BudgetManualName).MaximumLength(200);
        RuleFor(x => x.BudgetManualAmount).GreaterThanOrEqualTo(0).When(x => x.BudgetManualAmount.HasValue);
    }
}

public class CreateContractCommandHandler(
    IApplicationDbContext db,
    ICurrentUser currentUser,
    IContractWorkflowService workflow,
    IContractCodeGenerator codeGenerator,
    IChangelogService changelog) : IRequestHandler<CreateContractCommand, Guid>
{
    public async Task<Guid> Handle(CreateContractCommand request, CancellationToken ct)
    {
        // Load supplier + project full (cần Code cho gen mã RG-001)
        var supplier = await db.Suppliers.FirstOrDefaultAsync(s => s.Id == request.SupplierId, ct)
            ?? throw new NotFoundException("Supplier", request.SupplierId);
        var project = await db.Projects.FirstOrDefaultAsync(p => p.Id == request.ProjectId, ct)
            ?? throw new NotFoundException("Project", request.ProjectId);

        var activeWfId = await db.WorkflowDefinitions.AsNoTracking()
            .Where(w => w.ContractType == request.Type && w.IsActive)
            .Select(w => (Guid?)w.Id)
            .FirstOrDefaultAsync(ct);

        // [Plan B S29 2026-05-22 Hotfix Reviewer] Validate ApprovalWorkflowId V2
        // (Mig 32) — User chọn lúc create. Phải tồn tại + ApplicableType=Contract(3).
        // Mirror PE pattern PurchaseEvaluationFeatures.cs:62-77. Defense-in-depth:
        // FE Workspace dropdown đã filter ApplicableType=3 server-side; BE guard
        // chặn attacker forge POST với PE workflow ID (ApplicableType=1/2).
        if (request.ApprovalWorkflowId is Guid awId)
        {
            var aw = await db.ApprovalWorkflows.AsNoTracking()
                .FirstOrDefaultAsync(w => w.Id == awId, ct)
                ?? throw new NotFoundException("ApprovalWorkflow", awId);
            if (aw.ApplicableType != Domain.ApprovalWorkflowsV2.ApprovalWorkflowApplicableType.Contract)
                throw new ConflictException(
                    $"Quy trình {aw.Code} áp dụng cho {aw.ApplicableType}, không khớp với HĐ (cần ApplicableType=Contract).");
        }

        var entity = new Contract
        {
            Type = request.Type,
            Phase = ContractPhase.DangSoanThao,
            SupplierId = request.SupplierId,
            ProjectId = request.ProjectId,
            DepartmentId = request.DepartmentId,
            DrafterUserId = currentUser.UserId,
            TemplateId = request.TemplateId,
            GiaTri = request.GiaTri,
            TenHopDong = request.TenHopDong,
            NoiDung = request.NoiDung,
            BypassProcurementAndCCM = request.BypassProcurementAndCCM,
            DraftData = request.DraftData,
            BudgetManualName = request.BudgetManualName,
            BudgetManualAmount = request.BudgetManualAmount,
            WorkflowDefinitionId = activeWfId,
            // [Plan B S29 2026-05-22 Chunk E1] Pin V2 workflow nếu Drafter pick
            // qua Workspace Select dropdown (Chunk D FE). Cả 2 set ok — Service
            // ApproveV2Async branch dispatch theo ApprovalWorkflowId trước.
            ApprovalWorkflowId = request.ApprovalWorkflowId,
            SlaDeadline = DateTime.UtcNow.Add(workflow.GetPhaseSla(ContractPhase.DangSoanThao) ?? TimeSpan.FromDays(7)),
        };

        // Gen mã HĐ NGAY tại create (user request) — không đợi DangDongDau như cũ.
        // Trade-off: nếu HĐ bị TuChoi → mã bị "wasted" (gap trong sequence). Acceptable
        // vì user muốn có mã ngay từ đầu để reference vào tài liệu/giấy tờ.
        // GenerateAsync chạy SERIALIZABLE transaction riêng — entity chưa tracked nên
        // không bị save kèm.
        entity.MaHopDong = await codeGenerator.GenerateAsync(entity, project.Code, supplier.Code, ct);

        db.Contracts.Add(entity);

        await changelog.LogContractChangeAsync(
            entity.Id,
            ChangelogAction.Insert,
            summary: $"Tạo HĐ {entity.MaHopDong} — {entity.TenHopDong ?? "(chưa tên)"}",
            phaseAtChange: entity.Phase,
            ct: ct);

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

// ========== UPDATE draft ==========

public record UpdateContractDraftCommand(
    Guid Id,
    decimal GiaTri,
    string? TenHopDong,
    string? NoiDung,
    Guid? TemplateId,
    string? DraftData,
    // [S61 Mig 50] BudgetId DROP — GIỮ BudgetManual* (HĐ nhập tay không đổi).
    string? BudgetManualName,
    decimal? BudgetManualAmount) : IRequest;

public class UpdateContractDraftCommandHandler(
    IApplicationDbContext db,
    IChangelogService changelog) : IRequestHandler<UpdateContractDraftCommand>
{
    public async Task Handle(UpdateContractDraftCommand request, CancellationToken ct)
    {
        var entity = await db.Contracts.FirstOrDefaultAsync(c => c.Id == request.Id, ct)
            ?? throw new NotFoundException("Contract", request.Id);

        if (entity.Phase != ContractPhase.DangSoanThao)
            throw new ConflictException("Chỉ được sửa HĐ khi ở phase Đang soạn thảo.");

        // Capture diff trước update để log
        var changes = new List<object>();
        if (entity.GiaTri != request.GiaTri)
            changes.Add(new { Field = "GiaTri", Old = entity.GiaTri, New = request.GiaTri });
        if (entity.TenHopDong != request.TenHopDong)
            changes.Add(new { Field = "TenHopDong", Old = entity.TenHopDong, New = request.TenHopDong });
        if (entity.NoiDung != request.NoiDung)
            changes.Add(new { Field = "NoiDung", Old = entity.NoiDung, New = request.NoiDung });
        if (entity.TemplateId != request.TemplateId)
            changes.Add(new { Field = "TemplateId", Old = entity.TemplateId, New = request.TemplateId });
        if (entity.BudgetManualName != request.BudgetManualName)
            changes.Add(new { Field = "BudgetManualName", Old = entity.BudgetManualName, New = request.BudgetManualName });
        if (entity.BudgetManualAmount != request.BudgetManualAmount)
            changes.Add(new { Field = "BudgetManualAmount", Old = entity.BudgetManualAmount, New = request.BudgetManualAmount });

        entity.GiaTri = request.GiaTri;
        entity.TenHopDong = request.TenHopDong;
        entity.NoiDung = request.NoiDung;
        entity.TemplateId = request.TemplateId;
        entity.DraftData = request.DraftData;
        entity.BudgetManualName = request.BudgetManualName;
        entity.BudgetManualAmount = request.BudgetManualAmount;

        if (changes.Count > 0)
        {
            await changelog.LogContractChangeAsync(
                entity.Id,
                ChangelogAction.Update,
                summary: $"Cập nhật HĐ ({changes.Count} field)",
                fieldChangesJson: System.Text.Json.JsonSerializer.Serialize(changes),
                phaseAtChange: entity.Phase,
                ct: ct);
        }

        await db.SaveChangesAsync(ct);
    }
}

// ========== TRANSITION phase ==========

public record TransitionContractCommand(
    Guid Id,
    ContractPhase TargetPhase,
    ApprovalDecision Decision,
    string? Comment) : IRequest;

public class TransitionContractCommandValidator : AbstractValidator<TransitionContractCommand>
{
    public TransitionContractCommandValidator()
    {
        RuleFor(x => x.Id).NotEmpty();
        RuleFor(x => x.TargetPhase).IsInEnum();
        RuleFor(x => x.Decision).IsInEnum();
        RuleFor(x => x.Comment).MaximumLength(1000);
    }
}

public class TransitionContractCommandHandler(
    IApplicationDbContext db,
    ICurrentUser currentUser,
    IContractWorkflowService workflow) : IRequestHandler<TransitionContractCommand>
{
    public async Task Handle(TransitionContractCommand request, CancellationToken ct)
    {
        if (!currentUser.IsAuthenticated || currentUser.UserId is null)
            throw new UnauthorizedException();

        var entity = await db.Contracts.FirstOrDefaultAsync(c => c.Id == request.Id, ct)
            ?? throw new NotFoundException("Contract", request.Id);

        await workflow.TransitionAsync(
            entity,
            request.TargetPhase,
            currentUser.UserId,
            currentUser.Roles,
            request.Decision,
            request.Comment,
            ct: ct);
    }
}

// ========== ADD comment ==========

public record AddCommentCommand(Guid ContractId, string Content) : IRequest<Guid>;

public class AddCommentCommandValidator : AbstractValidator<AddCommentCommand>
{
    public AddCommentCommandValidator()
    {
        RuleFor(x => x.ContractId).NotEmpty();
        RuleFor(x => x.Content).NotEmpty().MaximumLength(2000);
    }
}

public class AddCommentCommandHandler(
    IApplicationDbContext db,
    ICurrentUser currentUser,
    IChangelogService changelog) : IRequestHandler<AddCommentCommand, Guid>
{
    public async Task<Guid> Handle(AddCommentCommand request, CancellationToken ct)
    {
        if (!currentUser.IsAuthenticated || currentUser.UserId is null)
            throw new UnauthorizedException();

        var contract = await db.Contracts.FirstOrDefaultAsync(c => c.Id == request.ContractId, ct)
            ?? throw new NotFoundException("Contract", request.ContractId);

        var comment = new ContractComment
        {
            ContractId = request.ContractId,
            UserId = currentUser.UserId.Value,
            Phase = contract.Phase,
            Content = request.Content,
        };
        db.ContractComments.Add(comment);
        await changelog.LogCommentAddedAsync(request.ContractId, request.Content, contract.Phase, ct);
        await db.SaveChangesAsync(ct);
        return comment.Id;
    }
}

// ========== LIST contracts (admin view) ==========

public record ListContractsQuery(
    ContractPhase? Phase = null,
    Guid? SupplierId = null,
    Guid? ProjectId = null) : PagedRequest, IRequest<PagedResult<ContractListItemDto>>;

public class ListContractsQueryHandler(IApplicationDbContext db, ICurrentUser currentUser)
    : IRequestHandler<ListContractsQuery, PagedResult<ContractListItemDto>>
{
    public async Task<PagedResult<ContractListItemDto>> Handle(ListContractsQuery request, CancellationToken ct)
    {
        var q = from c in db.Contracts.AsNoTracking()
                join s in db.Suppliers.AsNoTracking() on c.SupplierId equals s.Id
                join p in db.Projects.AsNoTracking() on c.ProjectId equals p.Id
                select new { c, s, p };

        // IDOR: non-admin chỉ thấy HĐ mình là Drafter hoặc role eligible phase hiện tại
        if (!currentUser.Roles.Contains(AppRoles.Admin))
        {
            var userId = currentUser.UserId;
            var eligiblePhases = GetEligiblePhases(currentUser.Roles);
            q = q.Where(x => x.c.DrafterUserId == userId || eligiblePhases.Contains(x.c.Phase));
        }

        if (request.Phase is not null) q = q.Where(x => x.c.Phase == request.Phase);
        if (request.SupplierId is not null) q = q.Where(x => x.c.SupplierId == request.SupplierId);
        if (request.ProjectId is not null) q = q.Where(x => x.c.ProjectId == request.ProjectId);

        if (!string.IsNullOrWhiteSpace(request.Search))
        {
            var s = request.Search.Trim();
            q = q.Where(x =>
                (x.c.MaHopDong != null && x.c.MaHopDong.Contains(s)) ||
                (x.c.TenHopDong != null && x.c.TenHopDong.Contains(s)) ||
                x.s.Name.Contains(s) || x.p.Name.Contains(s));
        }

        q = request.SortDesc ? q.OrderByDescending(x => x.c.CreatedAt) : q.OrderBy(x => x.c.CreatedAt);

        var total = await q.CountAsync(ct);
        var items = await q
            .Skip((request.Page - 1) * request.PageSize).Take(request.PageSize)
            .Select(x => new ContractListItemDto(
                x.c.Id, x.c.MaHopDong, x.c.TenHopDong, x.c.Type, x.c.Phase,
                x.c.SupplierId, x.s.Name,
                x.c.ProjectId, x.p.Name,
                x.c.GiaTri, x.c.SlaDeadline, x.c.CreatedAt))
            .ToListAsync(ct);

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

    // Mirror GetMyInboxQueryHandler.PhaseActorRoles — shared qua internal static
    internal static List<ContractPhase> GetEligiblePhases(IReadOnlyList<string> userRoles)
    {
        var phases = new HashSet<ContractPhase>();
        void AddIfAny(string[] required, params ContractPhase[] toAdd)
        {
            if (userRoles.Any(r => required.Contains(r)))
                foreach (var p in toAdd) phases.Add(p);
        }
        AddIfAny([AppRoles.Drafter, AppRoles.DeptManager],
            ContractPhase.DangSoanThao, ContractPhase.DangDamPhan, ContractPhase.DangInKy);
        AddIfAny([AppRoles.ProjectManager, AppRoles.Procurement, AppRoles.CostControl,
                  AppRoles.Finance, AppRoles.Accounting, AppRoles.Equipment],
            ContractPhase.DangGopY);
        AddIfAny([AppRoles.CostControl], ContractPhase.DangKiemTraCCM);
        AddIfAny([AppRoles.Director, AppRoles.AuthorizedSigner], ContractPhase.DangTrinhKy);
        AddIfAny([AppRoles.HrAdmin], ContractPhase.DangDongDau);
        return phases.ToList();
    }
}

// ========== INBOX — HĐ chờ role/tôi xử lý ==========

public record GetMyInboxQuery : IRequest<List<ContractListItemDto>>;

public class GetMyInboxQueryHandler(
    IApplicationDbContext db,
    ICurrentUser currentUser) : IRequestHandler<GetMyInboxQuery, List<ContractListItemDto>>
{
    // Map phase → role nào được xử lý (xem workflow-contract.md)
    private static readonly Dictionary<ContractPhase, string[]> PhaseActorRoles = new()
    {
        [ContractPhase.DangSoanThao] = [AppRoles.Drafter, AppRoles.DeptManager],
        [ContractPhase.DangGopY] = [AppRoles.ProjectManager, AppRoles.Procurement, AppRoles.CostControl, AppRoles.Finance, AppRoles.Accounting, AppRoles.Equipment],
        [ContractPhase.DangDamPhan] = [AppRoles.Drafter, AppRoles.DeptManager, AppRoles.ProjectManager],
        [ContractPhase.DangInKy] = [AppRoles.Drafter, AppRoles.DeptManager, AppRoles.ProjectManager],
        [ContractPhase.DangKiemTraCCM] = [AppRoles.CostControl],
        [ContractPhase.DangTrinhKy] = [AppRoles.Director, AppRoles.AuthorizedSigner],
        [ContractPhase.DangDongDau] = [AppRoles.HrAdmin],
    };

    public async Task<List<ContractListItemDto>> Handle(GetMyInboxQuery request, CancellationToken ct)
    {
        if (!currentUser.IsAuthenticated) throw new UnauthorizedException();

        var userRoles = currentUser.Roles;
        var isAdmin = userRoles.Contains(AppRoles.Admin);

        // Phase phù hợp với role hiện tại (Admin thấy tất cả phase chưa kết thúc)
        var eligiblePhases = isAdmin
            ? PhaseActorRoles.Keys.ToList()
            : PhaseActorRoles
                .Where(kv => kv.Value.Any(r => userRoles.Contains(r)))
                .Select(kv => kv.Key)
                .ToList();

        if (eligiblePhases.Count == 0) return [];

        var q = from c in db.Contracts.AsNoTracking()
                join s in db.Suppliers.AsNoTracking() on c.SupplierId equals s.Id
                join p in db.Projects.AsNoTracking() on c.ProjectId equals p.Id
                where eligiblePhases.Contains(c.Phase)
                orderby c.SlaDeadline ?? DateTime.MaxValue
                select new ContractListItemDto(
                    c.Id, c.MaHopDong, c.TenHopDong, c.Type, c.Phase,
                    c.SupplierId, s.Name, c.ProjectId, p.Name,
                    c.GiaTri, c.SlaDeadline, c.CreatedAt);

        return await q.Take(100).ToListAsync(ct);
    }
}

// ========== GET detail ==========

public record GetContractQuery(Guid Id) : IRequest<ContractDetailDto>;

public class GetContractQueryHandler(
    IApplicationDbContext db,
    UserManager<User> userManager,
    ICurrentUser currentUser) : IRequestHandler<GetContractQuery, ContractDetailDto>
{
    public async Task<ContractDetailDto> Handle(GetContractQuery request, CancellationToken ct)
    {
        var c = await db.Contracts.AsNoTracking()
            .Include(x => x.Approvals)
            .Include(x => x.Comments)
            .Include(x => x.Attachments)
            .FirstOrDefaultAsync(x => x.Id == request.Id, ct)
            ?? throw new NotFoundException("Contract", request.Id);

        // IDOR guard: non-admin chỉ xem được HĐ mình là Drafter hoặc role eligible phase hiện tại
        var isAdmin = currentUser.Roles.Contains(AppRoles.Admin);
        if (!isAdmin)
        {
            var isDrafter = c.DrafterUserId == currentUser.UserId;
            var eligiblePhases = ListContractsQueryHandler.GetEligiblePhases(currentUser.Roles);
            var isEligibleByRole = eligiblePhases.Contains(c.Phase);
            if (!isDrafter && !isEligibleByRole)
                throw new ForbiddenException("Bạn không có quyền xem HĐ này.");
        }

        var supplier = await db.Suppliers.AsNoTracking().FirstOrDefaultAsync(s => s.Id == c.SupplierId, ct);
        var project = await db.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == c.ProjectId, ct);
        var department = c.DepartmentId is null ? null : await db.Departments.AsNoTracking().FirstOrDefaultAsync(d => d.Id == c.DepartmentId, ct);


        // Resolve workflow: pinned WorkflowDefinition > overrides > hardcoded
        WorkflowPolicy workflowPolicy;
        if (c.WorkflowDefinitionId is Guid wfId)
        {
            var def = await db.WorkflowDefinitions.AsNoTracking()
                .Include(d => d.Steps.OrderBy(s => s.Order))
                    .ThenInclude(s => s.Approvers)
                .FirstOrDefaultAsync(d => d.Id == wfId, ct);
            workflowPolicy = def is not null
                ? WorkflowPolicyRegistry.FromDefinition(def)
                : WorkflowPolicyRegistry.ForContract(c);
        }
        else
        {
            var workflowOverrides = await db.WorkflowTypeAssignments.AsNoTracking()
                .ToDictionaryAsync(a => a.ContractType, a => a.PolicyName, ct);
            workflowPolicy = WorkflowPolicyRegistry.ForContractWithOverrides(c, workflowOverrides);
        }

        // [Plan B S29 2026-05-22 Chunk E2] Load V2 LevelOpinions nếu pin ApprovalWorkflowId.
        // JOIN ApprovalWorkflowLevel + Step + Approver User để build dynamic DTO
        // cho FE Section 5 render dynamic theo flow shape. V1 legacy → empty list.
        List<ContractLevelOpinionDto>? levelOpinionsDto = null;
        if (c.ApprovalWorkflowId is Guid awIdLoad)
        {
            var opinions = await db.ContractLevelOpinions.AsNoTracking()
                .Where(o => o.ContractId == c.Id)
                .ToListAsync(ct);
            if (opinions.Count > 0)
            {
                var levelIds = opinions.Select(o => o.ApprovalWorkflowLevelId).ToHashSet();
                var levels = await db.ApprovalWorkflowLevels.AsNoTracking()
                    .Include(l => l.Step)
                    .Where(l => levelIds.Contains(l.Id))
                    .ToListAsync(ct);
                var approverUserIds = levels.Select(l => l.ApproverUserId).ToHashSet();
                var approverNames = await userManager.Users.AsNoTracking()
                    .Where(u => approverUserIds.Contains(u.Id))
                    .ToDictionaryAsync(u => u.Id, u => u.FullName, ct);
                levelOpinionsDto = opinions
                    .Select(o =>
                    {
                        var level = levels.FirstOrDefault(l => l.Id == o.ApprovalWorkflowLevelId);
                        var step = level?.Step;
                        return new ContractLevelOpinionDto(
                            o.Id,
                            o.ApprovalWorkflowLevelId,
                            StepOrder: step?.Order ?? 0,
                            StepName: step?.Name ?? "",
                            LevelOrder: level?.Order ?? 0,
                            LevelName: level?.Name,
                            ApproverUserId: level?.ApproverUserId ?? Guid.Empty,
                            ApproverFullName: level != null && approverNames.TryGetValue(level.ApproverUserId, out var afn) ? afn : null,
                            o.Comment ?? "",
                            o.SignedAt,
                            o.SignedByUserId,
                            o.SignedByFullName);
                    })
                    .OrderBy(d => d.StepOrder).ThenBy(d => d.LevelOrder)
                    .ToList();
            }
            else
            {
                levelOpinionsDto = new List<ContractLevelOpinionDto>();
            }
        }

        // Resolve user names
        var userIds = new HashSet<Guid>();
        if (c.DrafterUserId is Guid did) userIds.Add(did);
        foreach (var a in c.Approvals) if (a.ApproverUserId is Guid aid) userIds.Add(aid);
        foreach (var cm in c.Comments) userIds.Add(cm.UserId);
        var users = await userManager.Users.AsNoTracking()
            .Where(u => userIds.Contains(u.Id))
            .ToDictionaryAsync(u => u.Id, u => u.FullName, ct);

        return new ContractDetailDto(
            c.Id, c.MaHopDong, c.TenHopDong, c.NoiDung, c.Type, c.Phase,
            c.SupplierId, supplier?.Name ?? "",
            c.ProjectId, project?.Name ?? "",
            c.DepartmentId, department?.Name,
            c.DrafterUserId, c.DrafterUserId is Guid d && users.TryGetValue(d, out var dn) ? dn : null,
            c.TemplateId, c.GiaTri, c.BypassProcurementAndCCM, c.SlaDeadline, c.DraftData,
            c.CreatedAt, c.UpdatedAt,
            c.BudgetManualName, c.BudgetManualAmount,
            c.Approvals
                .OrderBy(a => a.ApprovedAt)
                .Select(a => new ContractApprovalDto(
                    a.Id, a.FromPhase, a.ToPhase, a.ApproverUserId,
                    a.ApproverUserId is Guid id && users.TryGetValue(id, out var n) ? n : null,
                    a.Decision, a.Comment, a.ApprovedAt))
                .ToList(),
            c.Comments
                .OrderBy(cm => cm.CreatedAt)
                .Select(cm => new ContractCommentDto(
                    cm.Id, cm.UserId,
                    users.TryGetValue(cm.UserId, out var cn) ? cn : "",
                    cm.Phase, cm.Content, cm.CreatedAt))
                .ToList(),
            c.Attachments
                .OrderBy(att => att.CreatedAt)
                .Select(att => new ContractAttachmentDto(
                    att.Id, att.FileName, att.StoragePath, att.FileSize,
                    att.ContentType, att.Purpose, att.Note, att.CreatedAt))
                .ToList(),
            BuildWorkflowSummary(c, workflowPolicy),
            // [Plan B S29 2026-05-22 Chunk E2] V2 fields
            ApprovalWorkflowId: c.ApprovalWorkflowId,
            CurrentApprovalLevelOrder: c.CurrentApprovalLevelOrder,
            LevelOpinions: levelOpinionsDto);
    }

    // FE uses this to render next-phase buttons dynamically — no more hardcoded
    // NEXT_PHASES map that silently drifts from the BE policy.
    private static WorkflowSummaryDto BuildWorkflowSummary(Contract c, WorkflowPolicy policy)
    {
        return new WorkflowSummaryDto(
            PolicyName: policy.Name,
            PolicyDescription: policy.Description,
            ActivePhases: policy.ActivePhases.ToList(),
            NextPhases: policy.NextPhasesFrom(c.Phase).ToList());
    }
}

// ========== DELETE (soft) ==========

public record DeleteContractCommand(Guid Id) : IRequest;

public class DeleteContractCommandHandler(IApplicationDbContext db) : IRequestHandler<DeleteContractCommand>
{
    public async Task Handle(DeleteContractCommand request, CancellationToken ct)
    {
        var entity = await db.Contracts.FirstOrDefaultAsync(c => c.Id == request.Id, ct)
            ?? throw new NotFoundException("Contract", request.Id);

        if (entity.Phase >= ContractPhase.DangInKy)
            throw new ConflictException("Không được xóa HĐ đã qua phase 'Đang in ký'.");

        db.Contracts.Remove(entity);
        await db.SaveChangesAsync(ct);
    }
}
