using ERPCore.Common.Http;
using ERPCore.Domain.Entities;
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common;
using ERPCore.Dtos.Vendors;
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 VendorService : IVendorService
{
    private readonly IRepository<Vendor> _vendors;
    private readonly IUnitOfWork _uow;

    public VendorService(IRepository<Vendor> vendors, IUnitOfWork uow)
    {
        _vendors = vendors;
        _uow = uow;
    }

    public async Task<PagedResponse<VendorDto>> ListAsync(PageQuery query, EntityStatus? status, CancellationToken ct = default)
    {
        var q = _vendors.Query().AsNoTracking();
        if (!string.IsNullOrWhiteSpace(query.Q))
        {
            var term = query.Q.Trim();
            q = q.Where(v => EF.Functions.ILike(v.Code, $"%{term}%") || EF.Functions.ILike(v.Name, $"%{term}%"));
        }
        if (status is not null) q = q.Where(v => v.Status == status);

        var total = await q.CountAsync(ct);
        var rows = await q.OrderBy(v => v.Code)
            .Skip(query.Skip).Take(query.PageSize)
            .ToListAsync(ct);

        return PagedResponse<VendorDto>.Create(rows.Select(Map).ToList(), query.Page, query.PageSize, total);
    }

    public async Task<ETagged<VendorDto>?> GetAsync(int vendorId, CancellationToken ct = default)
    {
        var vendor = await _vendors.Query().AsNoTracking()
            .FirstOrDefaultAsync(v => v.VendorId == vendorId, ct);
        return vendor is null ? null : new ETagged<VendorDto>(Map(vendor), vendor.RowVersion);
    }

    public async Task<ETagged<VendorDto>> CreateAsync(CreateVendorRequest request, CancellationToken ct = default)
    {
        var code = request.Code.Trim();
        if (await _vendors.Query().AnyAsync(v => v.Code == code, ct))
            throw new ConflictException($"A vendor with code '{code}' already exists.");

        var vendor = new Vendor
        {
            Code = code,
            Name = request.Name.Trim(),
            Terms = request.Terms,
            TaxReg = request.TaxReg,
            Currency = request.Currency.Trim().ToUpperInvariant(),
            Status = EntityStatus.Active,
            CreatedAt = DateTime.UtcNow
        };

        await _vendors.AddAsync(vendor, ct);
        await _uow.SaveChangesAsync(ct);

        return new ETagged<VendorDto>(Map(vendor), vendor.RowVersion);
    }

    public async Task<ETagged<VendorDto>> UpdateAsync(
        int vendorId, UpdateVendorRequest request, uint expectedRowVersion, CancellationToken ct = default)
    {
        var vendor = await _vendors.GetByIdAsync(vendorId, ct)
            ?? throw new NotFoundException($"Vendor {vendorId} was not found.");

        if (vendor.RowVersion != expectedRowVersion)
            throw new DomainException(ErrorCodes.ConcurrencyConflict, "The vendor was modified by another request.", 412);

        var code = request.Code.Trim();
        if (!string.Equals(vendor.Code, code, StringComparison.Ordinal)
            && await _vendors.Query().AnyAsync(v => v.Code == code && v.VendorId != vendorId, ct))
            throw new ConflictException($"A vendor with code '{code}' already exists.");

        vendor.Code = code;
        vendor.Name = request.Name.Trim();
        vendor.Terms = request.Terms;
        vendor.TaxReg = request.TaxReg;
        vendor.Currency = request.Currency.Trim().ToUpperInvariant();
        vendor.UpdatedAt = DateTime.UtcNow;

        try
        {
            await _uow.SaveChangesAsync(ct);
        }
        catch (DbUpdateConcurrencyException)
        {
            throw new DomainException(ErrorCodes.ConcurrencyConflict, "The vendor was modified by another request.", 412);
        }

        return new ETagged<VendorDto>(Map(vendor), vendor.RowVersion);
    }

    public async Task SetStatusAsync(int vendorId, EntityStatus status, CancellationToken ct = default)
    {
        var vendor = await _vendors.GetByIdAsync(vendorId, ct)
            ?? throw new NotFoundException($"Vendor {vendorId} was not found.");

        vendor.Status = status;
        vendor.UpdatedAt = DateTime.UtcNow;
        await _uow.SaveChangesAsync(ct);
    }

    private static VendorDto Map(Vendor v) => new(
        v.VendorId, v.Code, v.Name, v.Terms, v.TaxReg, v.Currency, v.Status, v.CreatedAt, v.UpdatedAt);
}
