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

namespace ERPCore.Services;

/// <summary>
/// Item master service. Enforces SKU uniqueness (SKU_DUPLICATE), reference
/// integrity, and optimistic concurrency (CONCURRENCY_CONFLICT) per
/// docs/11-BACKEND-PHASE1.md §2.1–2.2 and 02-SECURITY C.1.
/// </summary>
public sealed class ItemService : IItemService
{
    private readonly IRepository<Item> _items;
    private readonly IRepository<Category> _categories;
    private readonly IRepository<Uom> _uoms;
    private readonly IRepository<Vendor> _vendors;
    private readonly IRepository<Warehouse> _warehouses;
    private readonly IUnitOfWork _uow;

    public ItemService(
        IRepository<Item> items,
        IRepository<Category> categories,
        IRepository<Uom> uoms,
        IRepository<Vendor> vendors,
        IRepository<Warehouse> warehouses,
        IUnitOfWork uow)
    {
        _items = items;
        _categories = categories;
        _uoms = uoms;
        _vendors = vendors;
        _warehouses = warehouses;
        _uow = uow;
    }

    public async Task<PagedResponse<ItemListItemDto>> ListAsync(
        PageQuery query, EntityStatus? status, int? categoryId, TrackingMode? trackingMode, CancellationToken ct = default)
    {
        var q = _items.Query().AsNoTracking();

        if (!string.IsNullOrWhiteSpace(query.Q))
        {
            var term = query.Q.Trim();
            q = q.Where(i => EF.Functions.ILike(i.Sku, $"%{term}%") || EF.Functions.ILike(i.Name, $"%{term}%"));
        }
        if (status is not null) q = q.Where(i => i.Status == status);
        if (categoryId is not null) q = q.Where(i => i.CategoryId == categoryId);
        if (trackingMode is not null) q = q.Where(i => i.TrackingMode == trackingMode);

        var total = await q.CountAsync(ct);
        var rows = await q.OrderBy(i => i.Sku)
            .Skip(query.Skip).Take(query.PageSize)
            .Select(i => new ItemListItemDto(
                i.ItemId, i.Sku, i.Name, i.CategoryId, i.BaseUomId, i.DefaultVendorId,
                i.ItemType, i.TrackingMode, i.TaxClass, i.Status))
            .ToListAsync(ct);

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

    public async Task<ETagged<ItemDetailDto>?> GetAsync(int itemId, CancellationToken ct = default)
    {
        var item = await _items.Query().AsNoTracking()
            .Include(i => i.ReorderSettings)
            .FirstOrDefaultAsync(i => i.ItemId == itemId, ct);

        return item is null ? null : new ETagged<ItemDetailDto>(ToDetail(item), item.RowVersion);
    }

    public async Task<ETagged<ItemDetailDto>> CreateAsync(CreateItemRequest request, CancellationToken ct = default)
    {
        if (await _items.Query().AnyAsync(i => i.Sku == request.Sku, ct))
            throw new DomainException(ErrorCodes.SkuDuplicate, $"An item with SKU '{request.Sku}' already exists.", 400);

        await ValidateReferencesAsync(request.CategoryId, request.BaseUomId, request.DefaultVendorId, ct);

        var item = new Item
        {
            Sku = request.Sku.Trim(),
            Name = request.Name.Trim(),
            Description = request.Description,
            CategoryId = request.CategoryId,
            BaseUomId = request.BaseUomId,
            DefaultVendorId = request.DefaultVendorId,
            ItemType = request.ItemType,
            TrackingMode = request.TrackingMode,
            TaxClass = request.TaxClass,
            Status = EntityStatus.Active,
            CreatedAt = DateTime.UtcNow
        };

        await _items.AddAsync(item, ct);
        await _uow.SaveChangesAsync(ct);

        return new ETagged<ItemDetailDto>(ToDetail(item), item.RowVersion);
    }

    public async Task<ETagged<ItemDetailDto>> UpdateAsync(
        int itemId, UpdateItemRequest request, uint expectedRowVersion, CancellationToken ct = default)
    {
        var item = await _items.Query()
            .Include(i => i.ReorderSettings)
            .FirstOrDefaultAsync(i => i.ItemId == itemId, ct)
            ?? throw new NotFoundException($"Item {itemId} was not found.");

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

        if (!string.Equals(item.Sku, request.Sku, StringComparison.Ordinal)
            && await _items.Query().AnyAsync(i => i.Sku == request.Sku && i.ItemId != itemId, ct))
            throw new DomainException(ErrorCodes.SkuDuplicate, $"An item with SKU '{request.Sku}' already exists.", 400);

        await ValidateReferencesAsync(request.CategoryId, request.BaseUomId, request.DefaultVendorId, ct);

        item.Sku = request.Sku.Trim();
        item.Name = request.Name.Trim();
        item.Description = request.Description;
        item.CategoryId = request.CategoryId;
        item.BaseUomId = request.BaseUomId;
        item.DefaultVendorId = request.DefaultVendorId;
        item.ItemType = request.ItemType;
        item.TrackingMode = request.TrackingMode;
        item.TaxClass = request.TaxClass;
        item.UpdatedAt = DateTime.UtcNow;

        await SaveGuardingConcurrencyAsync(ct);
        return new ETagged<ItemDetailDto>(ToDetail(item), item.RowVersion);
    }

    public async Task SetStatusAsync(int itemId, EntityStatus status, CancellationToken ct = default)
    {
        var item = await _items.GetByIdAsync(itemId, ct)
            ?? throw new NotFoundException($"Item {itemId} was not found.");

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

    public async Task<ItemReorderSettingsDto> UpdateReorderAsync(
        int itemId, UpdateReorderRequest request, CancellationToken ct = default)
    {
        if (request.Settings.Select(s => s.WarehouseId).Distinct().Count() != request.Settings.Count)
            throw new DomainException(ErrorCodes.Validation, "Duplicate warehouseId in reorder settings.", 400);

        var item = await _items.Query()
            .Include(i => i.ReorderSettings)
            .FirstOrDefaultAsync(i => i.ItemId == itemId, ct)
            ?? throw new NotFoundException($"Item {itemId} was not found.");

        foreach (var warehouseId in request.Settings.Select(s => s.WarehouseId))
            if (!await _warehouses.Query().AnyAsync(w => w.WarehouseId == warehouseId, ct))
                throw new DomainException(ErrorCodes.Validation, $"Warehouse {warehouseId} does not exist.", 422);

        // Full-replacement upsert (avoids delete+insert clashes on the unique index).
        foreach (var stale in item.ReorderSettings.Where(r => request.Settings.All(s => s.WarehouseId != r.WarehouseId)).ToList())
            item.ReorderSettings.Remove(stale);
        foreach (var input in request.Settings)
        {
            var existing = item.ReorderSettings.FirstOrDefault(r => r.WarehouseId == input.WarehouseId);
            if (existing is null)
            {
                item.ReorderSettings.Add(new ItemReorder
                {
                    ItemId = itemId,
                    WarehouseId = input.WarehouseId,
                    ReorderPoint = input.ReorderPoint,
                    ReorderQty = input.ReorderQty
                });
            }
            else
            {
                existing.ReorderPoint = input.ReorderPoint;
                existing.ReorderQty = input.ReorderQty;
            }
        }

        item.UpdatedAt = DateTime.UtcNow;
        await _uow.SaveChangesAsync(ct);

        var settings = item.ReorderSettings
            .OrderBy(r => r.WarehouseId)
            .Select(r => new ItemReorderDto(r.WarehouseId, r.ReorderPoint, r.ReorderQty))
            .ToList();
        return new ItemReorderSettingsDto(settings);
    }

    public async Task<ItemUomConversionsDto> UpdateUomConversionsAsync(
        int itemId, UpdateUomConversionsRequest request, CancellationToken ct = default)
    {
        var pairs = request.Conversions.Select(c => (c.FromUom, c.ToUom)).ToList();
        if (pairs.Distinct().Count() != pairs.Count)
            throw new DomainException(ErrorCodes.Validation, "Duplicate (fromUom, toUom) in conversions.", 400);

        var item = await _items.Query()
            .Include(i => i.UomConversions)
            .FirstOrDefaultAsync(i => i.ItemId == itemId, ct)
            ?? throw new NotFoundException($"Item {itemId} was not found.");

        foreach (var uomId in request.Conversions.SelectMany(c => new[] { c.FromUom, c.ToUom }).Distinct())
            if (!await _uoms.Query().AnyAsync(u => u.UomId == uomId, ct))
                throw new DomainException(ErrorCodes.Validation, $"UOM {uomId} does not exist.", 422);

        foreach (var stale in item.UomConversions.Where(c => request.Conversions.All(r => r.FromUom != c.FromUomId || r.ToUom != c.ToUomId)).ToList())
            item.UomConversions.Remove(stale);
        foreach (var input in request.Conversions)
        {
            var existing = item.UomConversions.FirstOrDefault(c => c.FromUomId == input.FromUom && c.ToUomId == input.ToUom);
            if (existing is null)
            {
                item.UomConversions.Add(new UomConversion
                {
                    ItemId = itemId,
                    FromUomId = input.FromUom,
                    ToUomId = input.ToUom,
                    Factor = input.Factor
                });
            }
            else
            {
                existing.Factor = input.Factor;
            }
        }

        item.UpdatedAt = DateTime.UtcNow;
        await _uow.SaveChangesAsync(ct);

        var conversions = item.UomConversions
            .OrderBy(c => c.ConversionId)
            .Select(c => new UomConversionDto(c.ConversionId, c.FromUomId, c.ToUomId, c.Factor))
            .ToList();
        return new ItemUomConversionsDto(item.ItemId, item.BaseUomId, conversions);
    }

    private async Task ValidateReferencesAsync(int categoryId, int baseUomId, int? defaultVendorId, CancellationToken ct)
    {
        if (!await _categories.Query().AnyAsync(c => c.CategoryId == categoryId, ct))
            throw new DomainException(ErrorCodes.Validation, $"Category {categoryId} does not exist.", 422);

        if (!await _uoms.Query().AnyAsync(u => u.UomId == baseUomId, ct))
            throw new DomainException(ErrorCodes.Validation, $"UOM {baseUomId} does not exist.", 422);

        if (defaultVendorId is not null)
        {
            var vendor = await _vendors.Query().AsNoTracking()
                .FirstOrDefaultAsync(v => v.VendorId == defaultVendorId, ct);
            if (vendor is null)
                throw new DomainException(ErrorCodes.Validation, $"Vendor {defaultVendorId} does not exist.", 422);
            if (vendor.Status != EntityStatus.Active)
                throw new DomainException(ErrorCodes.Validation, $"Vendor {defaultVendorId} is inactive.", 422);
        }
    }

    private async Task SaveGuardingConcurrencyAsync(CancellationToken ct)
    {
        try
        {
            await _uow.SaveChangesAsync(ct);
        }
        catch (DbUpdateConcurrencyException)
        {
            throw new DomainException(ErrorCodes.ConcurrencyConflict, "The item was modified by another request.", 412);
        }
    }

    private static ItemDetailDto ToDetail(Item i) => new(
        i.ItemId, i.Sku, i.Name, i.Description, i.CategoryId, i.BaseUomId, i.DefaultVendorId,
        i.ItemType, i.TrackingMode, i.TaxClass, i.Status,
        i.ReorderSettings
            .OrderBy(r => r.WarehouseId)
            .Select(r => new ItemReorderDto(r.WarehouseId, r.ReorderPoint, r.ReorderQty))
            .ToList(),
        i.CreatedAt, i.UpdatedAt);
}
