using ERPCore.Common.Http;
using ERPCore.Domain.Entities;
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common;
using ERPCore.Dtos.Customers;
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 CustomerService : ICustomerService
{
    private readonly IRepository<Customer> _customers;
    private readonly IRepository<Warehouse> _warehouses;
    private readonly IUnitOfWork _uow;

    public CustomerService(IRepository<Customer> customers, IRepository<Warehouse> warehouses, IUnitOfWork uow)
    {
        _customers = customers;
        _warehouses = warehouses;
        _uow = uow;
    }

    public async Task<PagedResponse<CustomerDto>> ListAsync(PageQuery query, EntityStatus? status, CustomerType? customerType, CancellationToken ct = default)
    {
        var q = _customers.Query().AsNoTracking();
        if (!string.IsNullOrWhiteSpace(query.Q))
        {
            var term = query.Q.Trim();
            q = q.Where(c => EF.Functions.ILike(c.Name, $"%{term}%")
                             || EF.Functions.ILike(c.CustomerCode, $"%{term}%")
                             || (c.DisplayName != null && EF.Functions.ILike(c.DisplayName, $"%{term}%")));
        }
        if (status is not null) q = q.Where(c => c.Status == status);
        if (customerType is not null) q = q.Where(c => c.CustomerType == customerType);

        var total = await q.CountAsync(ct);
        var rows = await q.OrderBy(c => c.Name)
            .Skip(query.Skip).Take(query.PageSize)
            .Select(c => new CustomerDto(
                c.CustomerId, c.CustomerCode, c.CustomerType, c.Name, c.DisplayName, c.Phone, c.Email,
                c.AddressLine1, c.AddressLine2, c.City, c.Country, c.TaxRegistrationNo,
                c.CreditLimit, c.CreditDays, c.DefaultWarehouseId, c.Status, c.CreatedAt, c.UpdatedAt))
            .ToListAsync(ct);

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

    public async Task<ETagged<CustomerDto>?> GetAsync(int customerId, CancellationToken ct = default)
    {
        var customer = await _customers.Query().AsNoTracking()
            .FirstOrDefaultAsync(c => c.CustomerId == customerId, ct);
        return customer is null ? null : new ETagged<CustomerDto>(Map(customer), customer.RowVersion);
    }

    public async Task<ETagged<CustomerDto>> CreateAsync(CreateCustomerRequest request, CancellationToken ct = default)
    {
        var code = request.CustomerCode.Trim();
        var name = request.Name.Trim();

        if (await _customers.Query().AnyAsync(c => c.CustomerCode.ToLower() == code.ToLower(), ct))
            throw new ConflictException($"A customer code '{code}' already exists.");

        if (!string.IsNullOrWhiteSpace(request.Email))
        {
            var email = request.Email.Trim();
            if (await _customers.Query().AnyAsync(c => c.Email != null && c.Email.ToLower() == email.ToLower(), ct))
                throw new ConflictException($"A customer with email '{email}' already exists.");
        }

        if (request.DefaultWarehouseId is not null)
        {
            var exists = await _warehouses.Query().AnyAsync(w => w.WarehouseId == request.DefaultWarehouseId, ct);
            if (!exists) throw new NotFoundException($"Warehouse {request.DefaultWarehouseId} was not found.");
        }

        var customer = new Customer
        {
            CustomerCode = code,
            CustomerType = request.CustomerType,
            Name = name,
            DisplayName = Normalize(request.DisplayName),
            Phone = Normalize(request.Phone),
            Email = Normalize(request.Email),
            AddressLine1 = Normalize(request.AddressLine1),
            AddressLine2 = Normalize(request.AddressLine2),
            City = Normalize(request.City),
            Country = Normalize(request.Country),
            TaxRegistrationNo = Normalize(request.TaxRegistrationNo),
            CreditLimit = request.CreditLimit,
            CreditDays = request.CreditDays,
            DefaultWarehouseId = request.DefaultWarehouseId,
            Status = EntityStatus.Active,
            CreatedAt = DateTime.UtcNow
        };

        await _customers.AddAsync(customer, ct);
        await _uow.SaveChangesAsync(ct);

        return new ETagged<CustomerDto>(Map(customer), customer.RowVersion);
    }

    public async Task<ETagged<CustomerDto>> UpdateAsync(int customerId, UpdateCustomerRequest request, uint expectedRowVersion, CancellationToken ct = default)
    {
        var customer = await _customers.GetByIdAsync(customerId, ct)
            ?? throw new NotFoundException($"Customer {customerId} was not found.");

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

        var code = request.CustomerCode.Trim();
        var name = request.Name.Trim();

        if (!string.Equals(customer.CustomerCode, code, StringComparison.Ordinal)
            && await _customers.Query().AnyAsync(c => c.CustomerCode.ToLower() == code.ToLower() && c.CustomerId != customerId, ct))
            throw new ConflictException($"A customer code '{code}' already exists.");

        if (!string.Equals(customer.Email, request.Email?.Trim(), StringComparison.OrdinalIgnoreCase)
            && !string.IsNullOrWhiteSpace(request.Email)
            && await _customers.Query().AnyAsync(c => c.Email != null && c.Email.ToLower() == request.Email!.Trim().ToLower() && c.CustomerId != customerId, ct))
            throw new ConflictException($"A customer with email '{request.Email.Trim()}' already exists.");

        if (request.DefaultWarehouseId is not null)
        {
            var exists = await _warehouses.Query().AnyAsync(w => w.WarehouseId == request.DefaultWarehouseId, ct);
            if (!exists) throw new NotFoundException($"Warehouse {request.DefaultWarehouseId} was not found.");
        }

        customer.CustomerCode = code;
        customer.CustomerType = request.CustomerType;
        customer.Name = name;
        customer.DisplayName = Normalize(request.DisplayName);
        customer.Phone = Normalize(request.Phone);
        customer.Email = Normalize(request.Email);
        customer.AddressLine1 = Normalize(request.AddressLine1);
        customer.AddressLine2 = Normalize(request.AddressLine2);
        customer.City = Normalize(request.City);
        customer.Country = Normalize(request.Country);
        customer.TaxRegistrationNo = Normalize(request.TaxRegistrationNo);
        customer.CreditLimit = request.CreditLimit;
        customer.CreditDays = request.CreditDays;
        customer.DefaultWarehouseId = request.DefaultWarehouseId;
        customer.UpdatedAt = DateTime.UtcNow;

        await _uow.SaveChangesAsync(ct);
        return new ETagged<CustomerDto>(Map(customer), customer.RowVersion);
    }

    public async Task SetStatusAsync(int customerId, EntityStatus status, CancellationToken ct = default)
    {
        var customer = await _customers.GetByIdAsync(customerId, ct)
            ?? throw new NotFoundException($"Customer {customerId} was not found.");

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

    private static CustomerDto Map(Customer c) => new(
        c.CustomerId, c.CustomerCode, c.CustomerType, c.Name, c.DisplayName, c.Phone, c.Email,
        c.AddressLine1, c.AddressLine2, c.City, c.Country, c.TaxRegistrationNo,
        c.CreditLimit, c.CreditDays, c.DefaultWarehouseId, c.Status, c.CreatedAt, c.UpdatedAt);

    private static string? Normalize(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
