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.PurchaseEvaluations.Dtos;
using SolutionErp.Application.PurchaseEvaluations.Services;
using SolutionErp.Domain.Contracts;
using SolutionErp.Domain.Identity;
using SolutionErp.Domain.PurchaseEvaluations;

namespace SolutionErp.Application.PurchaseEvaluations;

// ========== CREATE ==========

public record CreatePurchaseEvaluationCommand(
    PurchaseEvaluationType Type,
    string TenGoiThau,
    Guid ProjectId,
    Guid? DepartmentId,
    string? DiaDiem,
    string? MoTa,
    string? PaymentTerms,
    Guid? BudgetId,
    string? BudgetManualName,
    decimal? BudgetManualAmount) : IRequest<Guid>;

public class CreatePurchaseEvaluationCommandValidator : AbstractValidator<CreatePurchaseEvaluationCommand>
{
    public CreatePurchaseEvaluationCommandValidator()
    {
        RuleFor(x => x.Type).IsInEnum();
        RuleFor(x => x.TenGoiThau).NotEmpty().MaximumLength(500);
        RuleFor(x => x.ProjectId).NotEmpty();
        RuleFor(x => x.DiaDiem).MaximumLength(500);
        RuleFor(x => x.MoTa).MaximumLength(2000);
        RuleFor(x => x.BudgetManualName).MaximumLength(200);
        RuleFor(x => x.BudgetManualAmount).GreaterThanOrEqualTo(0).When(x => x.BudgetManualAmount.HasValue);
    }
}

public class CreatePurchaseEvaluationCommandHandler(
    IApplicationDbContext db,
    ICurrentUser currentUser,
    IPurchaseEvaluationWorkflowService workflow,
    IPurchaseEvaluationCodeGenerator codeGen) : IRequestHandler<CreatePurchaseEvaluationCommand, Guid>
{
    public async Task<Guid> Handle(CreatePurchaseEvaluationCommand request, CancellationToken ct)
    {
        _ = await db.Projects.FirstOrDefaultAsync(p => p.Id == request.ProjectId, ct)
            ?? throw new NotFoundException("Project", request.ProjectId);

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

        // Validate Budget link (nếu có): cùng Project + Phase=DaDuyet (chỉ cho
        // pick ngân sách đã duyệt mới được dùng làm reference đối chiếu).
        if (request.BudgetId is Guid bid)
        {
            var bg = await db.Budgets.AsNoTracking()
                .FirstOrDefaultAsync(b => b.Id == bid, ct)
                ?? throw new NotFoundException("Budget", bid);
            if (bg.ProjectId != request.ProjectId)
                throw new ConflictException("Ngân sách phải cùng dự án với phiếu.");
            if (bg.Phase != Domain.Budgets.BudgetPhase.DaDuyet)
                throw new ConflictException("Chỉ link được ngân sách đã duyệt.");
        }

        var entity = new PurchaseEvaluation
        {
            Type = request.Type,
            Phase = PurchaseEvaluationPhase.DangSoanThao,
            TenGoiThau = request.TenGoiThau,
            ProjectId = request.ProjectId,
            DepartmentId = request.DepartmentId,
            DiaDiem = request.DiaDiem,
            MoTa = request.MoTa,
            DrafterUserId = currentUser.UserId,
            WorkflowDefinitionId = activeWfId,
            PaymentTerms = request.PaymentTerms,
            BudgetId = request.BudgetId,
            BudgetManualName = request.BudgetManualName,
            BudgetManualAmount = request.BudgetManualAmount,
            SlaDeadline = DateTime.UtcNow.Add(
                workflow.GetPhaseSla(PurchaseEvaluationPhase.DangSoanThao) ?? TimeSpan.FromDays(3)),
        };

        // Atomic MaPhieu sequence — format PE/{YYYY}/{TypeLetter}/{Seq:D3}
        // (mirror IContractCodeGenerator pattern, transaction SERIALIZABLE).
        entity.MaPhieu = await codeGen.GenerateAsync(entity, ct);

        db.PurchaseEvaluations.Add(entity);

        db.PurchaseEvaluationChangelogs.Add(new PurchaseEvaluationChangelog
        {
            PurchaseEvaluationId = entity.Id,
            EntityType = PurchaseEvaluationEntityType.Header,
            Action = ChangelogAction.Insert,
            PhaseAtChange = entity.Phase,
            UserId = currentUser.UserId,
            Summary = $"Tạo phiếu {entity.MaPhieu} — {entity.TenGoiThau}",
        });

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

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

public record UpdatePurchaseEvaluationDraftCommand(
    Guid Id,
    string TenGoiThau,
    string? DiaDiem,
    string? MoTa,
    string? PaymentTerms,
    Guid? BudgetId,
    string? BudgetManualName,
    decimal? BudgetManualAmount) : IRequest;

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

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

        // Validate Budget link nếu thay đổi.
        if (request.BudgetId is Guid bid && bid != entity.BudgetId)
        {
            var bg = await db.Budgets.AsNoTracking()
                .FirstOrDefaultAsync(b => b.Id == bid, ct)
                ?? throw new NotFoundException("Budget", bid);
            if (bg.ProjectId != entity.ProjectId)
                throw new ConflictException("Ngân sách phải cùng dự án với phiếu.");
            if (bg.Phase != Domain.Budgets.BudgetPhase.DaDuyet)
                throw new ConflictException("Chỉ link được ngân sách đã duyệt.");
        }

        entity.TenGoiThau = request.TenGoiThau;
        entity.DiaDiem = request.DiaDiem;
        entity.MoTa = request.MoTa;
        entity.PaymentTerms = request.PaymentTerms;
        entity.BudgetId = request.BudgetId;
        entity.BudgetManualName = request.BudgetManualName;
        entity.BudgetManualAmount = request.BudgetManualAmount;

        db.PurchaseEvaluationChangelogs.Add(new PurchaseEvaluationChangelog
        {
            PurchaseEvaluationId = entity.Id,
            EntityType = PurchaseEvaluationEntityType.Header,
            Action = ChangelogAction.Update,
            PhaseAtChange = entity.Phase,
            UserId = currentUser.UserId,
            Summary = "Cập nhật thông tin phiếu",
        });

        await db.SaveChangesAsync(ct);
    }
}

// ========== TRANSITION ==========

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

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

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

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

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

// ========== LIST ==========

public record ListPurchaseEvaluationsQuery(
    PurchaseEvaluationType? Type = null,
    PurchaseEvaluationPhase? Phase = null,
    Guid? ProjectId = null) : PagedRequest, IRequest<PagedResult<PurchaseEvaluationListItemDto>>;

public class ListPurchaseEvaluationsQueryHandler(
    IApplicationDbContext db,
    ICurrentUser currentUser) : IRequestHandler<ListPurchaseEvaluationsQuery, PagedResult<PurchaseEvaluationListItemDto>>
{
    public async Task<PagedResult<PurchaseEvaluationListItemDto>> Handle(
        ListPurchaseEvaluationsQuery request, CancellationToken ct)
    {
        var q = from e in db.PurchaseEvaluations.AsNoTracking()
                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()
                select new { e, p, s };

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

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

        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));
        }

        q = request.SortDesc ? q.OrderByDescending(x => x.e.CreatedAt) : q.OrderBy(x => 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.e.SelectedSupplierId, x.s != null ? x.s.Name : null,
                x.e.ContractId, x.e.SlaDeadline, x.e.CreatedAt))
            .ToListAsync(ct);

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

    internal static List<PurchaseEvaluationPhase> GetEligiblePhases(IReadOnlyList<string> userRoles)
    {
        var phases = new HashSet<PurchaseEvaluationPhase>();
        void AddIfAny(string[] required, params PurchaseEvaluationPhase[] toAdd)
        {
            if (userRoles.Any(r => required.Contains(r)))
                foreach (var p in toAdd) phases.Add(p);
        }
        AddIfAny([AppRoles.Drafter, AppRoles.DeptManager], PurchaseEvaluationPhase.DangSoanThao);
        AddIfAny([AppRoles.Procurement], PurchaseEvaluationPhase.ChoPurchasing);
        AddIfAny([AppRoles.ProjectManager], PurchaseEvaluationPhase.ChoDuAn);
        AddIfAny([AppRoles.CostControl], PurchaseEvaluationPhase.ChoCCM);
        AddIfAny([AppRoles.Director, AppRoles.AuthorizedSigner],
            PurchaseEvaluationPhase.ChoCEODuyetPA, PurchaseEvaluationPhase.ChoCEODuyetNCC);
        return phases.ToList();
    }
}

// ========== INBOX ==========

public record GetMyPurchaseEvaluationInboxQuery(PurchaseEvaluationType? Type = null)
    : IRequest<List<PurchaseEvaluationListItemDto>>;

public class GetMyPurchaseEvaluationInboxQueryHandler(
    IApplicationDbContext db,
    ICurrentUser currentUser) : IRequestHandler<GetMyPurchaseEvaluationInboxQuery, List<PurchaseEvaluationListItemDto>>
{
    public async Task<List<PurchaseEvaluationListItemDto>> Handle(
        GetMyPurchaseEvaluationInboxQuery request, CancellationToken ct)
    {
        if (!currentUser.IsAuthenticated) throw new UnauthorizedException();

        var userRoles = currentUser.Roles;
        var isAdmin = userRoles.Contains(AppRoles.Admin);
        var eligiblePhases = isAdmin
            ? [
                PurchaseEvaluationPhase.DangSoanThao,
                PurchaseEvaluationPhase.ChoPurchasing,
                PurchaseEvaluationPhase.ChoDuAn,
                PurchaseEvaluationPhase.ChoCCM,
                PurchaseEvaluationPhase.ChoCEODuyetPA,
                PurchaseEvaluationPhase.ChoCEODuyetNCC,
            ]
            : ListPurchaseEvaluationsQueryHandler.GetEligiblePhases(userRoles);

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

        var q = from e in db.PurchaseEvaluations.AsNoTracking()
                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()
                where eligiblePhases.Contains(e.Phase)
                select new { e, p, s };

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

        return await q
            .OrderBy(x => x.e.SlaDeadline ?? DateTime.MaxValue)
            .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.e.SelectedSupplierId, x.s != null ? x.s.Name : null,
                x.e.ContractId, x.e.SlaDeadline, x.e.CreatedAt))
            .Take(100)
            .ToListAsync(ct);
    }
}

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

public record GetPurchaseEvaluationQuery(Guid Id) : IRequest<PurchaseEvaluationDetailBundleDto>;

public class GetPurchaseEvaluationQueryHandler(
    IApplicationDbContext db,
    UserManager<User> userManager,
    ICurrentUser currentUser) : IRequestHandler<GetPurchaseEvaluationQuery, PurchaseEvaluationDetailBundleDto>
{
    public async Task<PurchaseEvaluationDetailBundleDto> Handle(
        GetPurchaseEvaluationQuery request, CancellationToken ct)
    {
        var e = await db.PurchaseEvaluations.AsNoTracking()
            .Include(x => x.Suppliers)
            .Include(x => x.Details).ThenInclude(d => d.Quotes)
            .Include(x => x.Approvals)
            .Include(x => x.Attachments)
            .Include(x => x.DepartmentOpinions)
            .FirstOrDefaultAsync(x => x.Id == request.Id, ct)
            ?? throw new NotFoundException("PurchaseEvaluation", request.Id);

        var isAdmin = currentUser.Roles.Contains(AppRoles.Admin);
        if (!isAdmin)
        {
            var isDrafter = e.DrafterUserId == currentUser.UserId;
            var eligiblePhases = ListPurchaseEvaluationsQueryHandler.GetEligiblePhases(currentUser.Roles);
            if (!isDrafter && !eligiblePhases.Contains(e.Phase))
                throw new ForbiddenException("Bạn không có quyền xem phiếu này.");
        }

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

        // Load Budget summary nếu có link
        Budgets.Dtos.BudgetSummaryDto? budgetSummary = null;
        if (e.BudgetId is Guid budgetId)
        {
            budgetSummary = await db.Budgets.AsNoTracking()
                .Where(b => b.Id == budgetId)
                .Select(b => new Budgets.Dtos.BudgetSummaryDto(
                    b.Id, b.MaNganSach, b.TenNganSach, b.NamNganSach, b.Phase, b.TongNganSach))
                .FirstOrDefaultAsync(ct);
        }

        // Load supplier names for PE suppliers + approver names
        var supplierIds = e.Suppliers.Select(s => s.SupplierId).ToList();
        var suppliers = await db.Suppliers.AsNoTracking().Where(s => supplierIds.Contains(s.Id))
            .ToDictionaryAsync(s => s.Id, s => s.Name, ct);

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

        // Resolve workflow policy
        PurchaseEvaluationPolicy policy;
        if (e.WorkflowDefinitionId is Guid wfId)
        {
            var def = await db.PurchaseEvaluationWorkflowDefinitions.AsNoTracking()
                .Include(d => d.Steps.OrderBy(s => s.Order))
                    .ThenInclude(s => s.Approvers)
                .FirstOrDefaultAsync(d => d.Id == wfId, ct);
            policy = def is not null
                ? PurchaseEvaluationPolicyRegistry.FromDefinition(def)
                : PurchaseEvaluationPolicyRegistry.ForEvaluation(e);
        }
        else
        {
            policy = PurchaseEvaluationPolicyRegistry.ForEvaluation(e);
        }

        return new PurchaseEvaluationDetailBundleDto(
            e.Id, e.MaPhieu, e.Type, e.Phase, e.TenGoiThau, e.DiaDiem, e.MoTa,
            e.ProjectId, project?.Name ?? "",
            e.DepartmentId, department?.Name,
            e.DrafterUserId, e.DrafterUserId is Guid d && users.TryGetValue(d, out var dn) ? dn : null,
            e.SelectedSupplierId, selectedSupplier?.Name,
            e.ContractId,
            e.PaymentTerms, e.SlaDeadline, e.CreatedAt, e.UpdatedAt,
            e.BudgetId, budgetSummary,
            e.BudgetManualName, e.BudgetManualAmount,
            e.Suppliers
                .OrderBy(s => s.Order)
                .Select(s => new PurchaseEvaluationSupplierDto(
                    s.Id, s.SupplierId,
                    suppliers.TryGetValue(s.SupplierId, out var sn) ? sn : "",
                    s.DisplayName, s.ContactName, s.ContactEmail, s.ContactPhone,
                    s.PaymentTermText, s.Note, s.Order))
                .ToList(),
            e.Details
                .OrderBy(d => d.Order)
                .Select(d => new PurchaseEvaluationDetailDto(
                    d.Id, d.GroupCode, d.GroupName, d.ItemCode, d.NoiDung, d.DonViTinh,
                    d.KhoiLuongNganSach, d.KhoiLuongThiCong, d.DonGiaNganSach, d.ThanhTienNganSach,
                    d.Order, d.GhiChu,
                    d.Quotes.Select(q => new PurchaseEvaluationQuoteDto(
                        q.Id, q.PurchaseEvaluationDetailId, q.PurchaseEvaluationSupplierId,
                        q.BgVat, q.ChuaVat, q.ThanhTien, q.IsSelected, q.Note)).ToList()))
                .ToList(),
            e.Approvals
                .OrderBy(a => a.ApprovedAt)
                .Select(a => new PurchaseEvaluationApprovalDto(
                    a.Id, a.FromPhase, a.ToPhase, a.ApproverUserId,
                    a.ApproverUserId is Guid uid && users.TryGetValue(uid, out var an) ? an : null,
                    a.Decision, a.Comment, a.ApprovedAt))
                .ToList(),
            e.Attachments
                .OrderByDescending(a => a.CreatedAt)
                .Select(a => new PurchaseEvaluationAttachmentDto(
                    a.Id, a.PurchaseEvaluationSupplierId, a.FileName, a.StoragePath,
                    a.FileSize, a.ContentType, a.Purpose, a.Note, a.CreatedAt))
                .ToList(),
            e.DepartmentOpinions
                .OrderBy(o => (int)o.Kind)
                .Select(o => new PurchaseEvaluationDepartmentOpinionDto(
                    o.Id, o.Kind, KindLabel(o.Kind),
                    o.Opinion, o.SignedAt, o.UserId, o.UserName))
                .ToList(),
            new PurchaseEvaluationWorkflowSummaryDto(
                policy.Name, policy.Description,
                policy.ActivePhases.ToList(),
                policy.NextPhasesFrom(e.Phase).ToList()));
    }

    private static string KindLabel(PeDepartmentKind k) => k switch
    {
        PeDepartmentKind.PheDuyet => "Phê duyệt",
        PeDepartmentKind.Ccm => "P.CCM",
        PeDepartmentKind.MuaHang => "P.Mua hàng",
        PeDepartmentKind.SmPm => "SM-PM",
        _ => k.ToString(),
    };
}

// ========== DELETE ==========

public record DeletePurchaseEvaluationCommand(Guid Id) : IRequest;

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

        if (entity.Phase != PurchaseEvaluationPhase.DangSoanThao
            && entity.Phase != PurchaseEvaluationPhase.TuChoi)
            throw new ConflictException("Chỉ xóa được phiếu ở phase Soạn thảo hoặc Từ chối.");

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

// ========== CHANGELOG list ==========

public record ListPurchaseEvaluationChangelogsQuery(Guid PurchaseEvaluationId, int Take = 200)
    : IRequest<List<PurchaseEvaluationChangelogDto>>;

public class ListPurchaseEvaluationChangelogsQueryHandler(IApplicationDbContext db)
    : IRequestHandler<ListPurchaseEvaluationChangelogsQuery, List<PurchaseEvaluationChangelogDto>>
{
    public async Task<List<PurchaseEvaluationChangelogDto>> Handle(
        ListPurchaseEvaluationChangelogsQuery request, CancellationToken ct)
    {
        return await db.PurchaseEvaluationChangelogs.AsNoTracking()
            .Where(c => c.PurchaseEvaluationId == request.PurchaseEvaluationId)
            .OrderByDescending(c => c.CreatedAt)
            .Take(request.Take)
            .Select(c => new PurchaseEvaluationChangelogDto(
                c.Id, c.EntityType, c.EntityId, c.Action, c.PhaseAtChange,
                c.UserId, c.UserName, c.Summary, c.FieldChangesJson, c.ContextNote,
                c.CreatedAt))
            .ToListAsync(ct);
    }
}
