using ERPCore.Domain;
using ERPCore.Domain.Entities;
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common;
using ERPCore.Dtos.Procurement;
using ERPCore.Infra.UoW;
using ERPCore.Repositories.Interfaces;
using ERPCore.Services.Interfaces;
using ERPCore.System.Errors;
using Microsoft.EntityFrameworkCore;

namespace ERPCore.Services;

public sealed class RfqService : IRfqService
{
    private readonly IRepository<Rfq> _rfqs;
    private readonly IRepository<Requisition> _requisitions;
    private readonly IRepository<Item> _items;
    private readonly IRepository<Vendor> _vendors;
    private readonly IRepository<VendorQuotation> _quotations;
    private readonly INumberSequenceService _numbers;
    private readonly IUnitOfWork _uow;

    public RfqService(
        IRepository<Rfq> rfqs, IRepository<Requisition> requisitions, IRepository<Item> items,
        IRepository<Vendor> vendors, IRepository<VendorQuotation> quotations,
        INumberSequenceService numbers, IUnitOfWork uow)
    {
        _rfqs = rfqs;
        _requisitions = requisitions;
        _items = items;
        _vendors = vendors;
        _quotations = quotations;
        _numbers = numbers;
        _uow = uow;
    }

    public async Task<PagedResponse<RfqSummaryDto>> ListAsync(
        PageQuery query, RfqStatus? status, CancellationToken ct = default)
    {
        var q = _rfqs.Query().AsNoTracking();

        if (!string.IsNullOrWhiteSpace(query.Q))
        {
            var term = query.Q.Trim();
            q = q.Where(r => EF.Functions.ILike(r.DocNo, $"%{term}%"));
        }
        if (status is not null) q = q.Where(r => r.Status == status);

        var total = await q.CountAsync(ct);
        var rows = await q.OrderByDescending(r => r.RfqId)
            .Skip(query.Skip).Take(query.PageSize)
            .Select(r => new RfqSummaryDto(
                r.RfqId, r.DocNo, r.RequisitionId, r.Status,
                r.Lines.Count,
                // Correlated subquery: there is no Rfq.Quotations navigation to count.
                _quotations.Query().Count(qt => qt.RfqId == r.RfqId)))
            .ToListAsync(ct);

        return PagedResponse<RfqSummaryDto>.Create(rows, query.Page, query.PageSize, total);
    }

    public async Task<RfqDto?> GetAsync(int rfqId, CancellationToken ct = default)
    {
        var rfq = await _rfqs.Query().AsNoTracking()
            .Include(r => r.Lines)
            .FirstOrDefaultAsync(r => r.RfqId == rfqId, ct);
        return rfq is null ? null : MapRfq(rfq);
    }

    public async Task<RfqDto> CreateAsync(CreateRfqRequest request, CancellationToken ct = default)
    {
        if (!await _requisitions.Query().AnyAsync(r => r.RequisitionId == request.RequisitionId, ct))
            throw new DomainException(ErrorCodes.Validation, $"Requisition {request.RequisitionId} does not exist.", 422);

        await EnsureItemsExistAsync(request.Lines.Select(l => l.ItemId), ct);
        await EnsureVendorsExistAsync(request.VendorIds, ct);

        var rfq = await _uow.ExecuteInTransactionAsync(async token =>
        {
            var docNo = await _numbers.NextAsync(DocumentTypes.Rfq, token);
            var entity = new Rfq
            {
                DocNo = docNo,
                RequisitionId = request.RequisitionId,
                Status = RfqStatus.Open,
                CreatedAt = DateTime.UtcNow,
                Lines = request.Lines.Select(l => new RfqLine { ItemId = l.ItemId, Qty = l.Qty }).ToList()
            };
            await _rfqs.AddAsync(entity, token);
            return entity;
        }, ct);

        return MapRfq(rfq);
    }

    public async Task<VendorQuotationDto> AddQuotationAsync(int rfqId, CreateQuotationRequest request, CancellationToken ct = default)
    {
        var rfq = await _rfqs.Query().AsNoTracking()
            .Include(r => r.Lines)
            .FirstOrDefaultAsync(r => r.RfqId == rfqId, ct)
            ?? throw new NotFoundException($"RFQ {rfqId} was not found.");

        if (!await _vendors.Query().AnyAsync(v => v.VendorId == request.VendorId, ct))
            throw new DomainException(ErrorCodes.Validation, $"Vendor {request.VendorId} does not exist.", 422);

        var rfqItemIds = rfq.Lines.Select(l => l.ItemId).ToHashSet();
        var offLine = request.Lines.Select(l => l.ItemId).FirstOrDefault(id => !rfqItemIds.Contains(id));
        if (offLine != 0)
            throw new DomainException(ErrorCodes.Validation, $"Item {offLine} is not part of RFQ {rfqId}.", 422);

        if (await _quotations.Query().AnyAsync(q => q.RfqId == rfqId && q.VendorId == request.VendorId, ct))
            throw new ConflictException($"Vendor {request.VendorId} has already quoted RFQ {rfqId}.");

        var quotation = new VendorQuotation
        {
            RfqId = rfqId,
            VendorId = request.VendorId,
            CreatedAt = DateTime.UtcNow,
            Lines = request.Lines.Select(l => new VendorQuotationLine
            {
                ItemId = l.ItemId,
                UnitPrice = l.UnitPrice,
                LeadDays = l.LeadDays
            }).ToList()
        };
        await _quotations.AddAsync(quotation, ct);
        await _uow.SaveChangesAsync(ct);

        return new VendorQuotationDto(
            quotation.QuotationId, quotation.RfqId, quotation.VendorId,
            quotation.Lines.Select(l => new QuotationLineDto(l.ItemId, l.UnitPrice, l.LeadDays)).ToList());
    }

    public async Task<RfqComparisonDto> GetComparisonAsync(int rfqId, CancellationToken ct = default)
    {
        var rfq = await _rfqs.Query().AsNoTracking()
            .Include(r => r.Lines)
            .Include(r => r.Quotations).ThenInclude(q => q.Lines)
            .FirstOrDefaultAsync(r => r.RfqId == rfqId, ct)
            ?? throw new NotFoundException($"RFQ {rfqId} was not found.");

        var vendorIds = rfq.Quotations.Select(q => q.VendorId).Distinct().OrderBy(v => v).ToList();

        var rows = rfq.Lines.OrderBy(l => l.RfqLineId).Select(line =>
        {
            var cells = rfq.Quotations
                .Select(q => new { q.VendorId, q.QuotationId, Line = q.Lines.FirstOrDefault(ql => ql.ItemId == line.ItemId) })
                .Where(x => x.Line is not null)
                .OrderBy(x => x.VendorId)
                .Select(x => new RfqComparisonCellDto(x.VendorId, x.QuotationId, x.Line!.UnitPrice, x.Line!.LeadDays))
                .ToList();
            return new RfqComparisonRowDto(line.ItemId, line.Qty, cells);
        }).ToList();

        return new RfqComparisonDto(rfqId, vendorIds, rows);
    }

    private async Task EnsureItemsExistAsync(IEnumerable<int> itemIds, CancellationToken ct)
    {
        var ids = itemIds.Distinct().ToList();
        var found = await _items.Query().AsNoTracking()
            .Where(i => ids.Contains(i.ItemId)).Select(i => i.ItemId).ToListAsync(ct);
        var missing = ids.Except(found).ToList();
        if (missing.Count > 0)
            throw new DomainException(ErrorCodes.Validation, $"Item(s) not found: {string.Join(", ", missing)}.", 422);
    }

    private async Task EnsureVendorsExistAsync(IEnumerable<int> vendorIds, CancellationToken ct)
    {
        var ids = vendorIds.Distinct().ToList();
        if (ids.Count == 0) return;
        var found = await _vendors.Query().AsNoTracking()
            .Where(v => ids.Contains(v.VendorId)).Select(v => v.VendorId).ToListAsync(ct);
        var missing = ids.Except(found).ToList();
        if (missing.Count > 0)
            throw new DomainException(ErrorCodes.Validation, $"Vendor(s) not found: {string.Join(", ", missing)}.", 422);
    }

    private static RfqDto MapRfq(Rfq r) => new(
        r.RfqId, r.DocNo, r.RequisitionId, r.Status,
        r.Lines.OrderBy(l => l.RfqLineId).Select(l => new RfqLineDto(l.RfqLineId, l.ItemId, l.Qty)).ToList());
}
