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, product-configuration gating (CONFIG_DISABLED), and optimistic
/// concurrency (CONCURRENCY_CONFLICT) per docs/11-BACKEND-PHASE1.md §2.1–2.2
/// and 02-SECURITY C.1.
/// <para>
/// The SKU arrives generated by the client (it encodes the chosen item-type values,
/// e.g. "BL-100-0003"); this service only checks that it is unique. Items hold no
/// item-type reference at all — see docs/10 Part C.9.
/// </para>
/// </summary>
public sealed class ItemService : IItemService
{
    private readonly IRepository<Item> _items;
    private readonly IRepository<Category> _categories;
    private readonly IRepository<SubCategory> _subCategories;
    private readonly IRepository<Brand> _brands;
    private readonly IRepository<Uom> _uoms;
    private readonly IRepository<Vendor> _vendors;
    private readonly IRepository<Warehouse> _warehouses;
    private readonly IRepository<StockLayer> _stockLayers;
    private readonly IRepository<StockLedger> _stockLedger;
    private readonly IProductConfigService _config;
    private readonly IUnitOfWork _uow;

    public ItemService(
        IRepository<Item> items,
        IRepository<Category> categories,
        IRepository<SubCategory> subCategories,
        IRepository<Brand> brands,
        IRepository<Uom> uoms,
        IRepository<Vendor> vendors,
        IRepository<Warehouse> warehouses,
        IRepository<StockLayer> stockLayers,
        IRepository<StockLedger> stockLedger,
        IProductConfigService config,
        IUnitOfWork uow)
    {
        _items = items;
        _categories = categories;
        _subCategories = subCategories;
        _brands = brands;
        _uoms = uoms;
        _vendors = vendors;
        _warehouses = warehouses;
        _stockLayers = stockLayers;
        _stockLedger = stockLedger;
        _config = config;
        _uow = uow;
    }

    public async Task<PagedResponse<ItemListItemDto>> ListAsync(
        PageQuery query, EntityStatus? status, int? categoryId, int? subCategoryId, int? brandId,
        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 (subCategoryId is not null) q = q.Where(i => i.SubCategoryId == subCategoryId);
        if (brandId is not null) q = q.Where(i => i.BrandId == brandId);
        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.SubCategoryId, i.BrandId,
                i.BaseUomId, i.DefaultVendorId,
                i.StockNature, i.TrackingMode, i.Warranty, i.WarrantyPeriodMonths, i.TaxClass, i.SalePrice,
                i.ContentQty, i.ContentUnit, i.ContentBaseQty, i.ContentBaseUnit,
                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.SubCategoryId, request.BrandId,
            request.BaseUomId, request.DefaultVendorId, ct);

        ItemContent.ValidatePair(request.ContentQty, request.ContentUnit);
        var (contentBaseQty, contentBaseUnit) = ItemContent.Normalize(request.ContentQty, request.ContentUnit);
        var warrantyPeriodMonths = ValidateWarrantyPeriod(request.Warranty, request.WarrantyPeriodMonths);

        var item = new Item
        {
            Sku = request.Sku.Trim(),
            Name = request.Name.Trim(),
            Description = request.Description,
            CategoryId = request.CategoryId,
            SubCategoryId = request.SubCategoryId,
            BrandId = request.BrandId,
            BaseUomId = request.BaseUomId,
            DefaultVendorId = request.DefaultVendorId,
            StockNature = request.StockNature,
            TrackingMode = request.TrackingMode,
            Warranty = request.Warranty,
            WarrantyPeriodMonths = warrantyPeriodMonths,
            TaxClass = request.TaxClass,
            SalePrice = request.SalePrice,
            ContentQty = request.ContentQty,
            ContentUnit = request.ContentUnit,
            ContentBaseQty = contentBaseQty,
            ContentBaseUnit = contentBaseUnit,
            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.SubCategoryId, request.BrandId,
            request.BaseUomId, request.DefaultVendorId, ct);

        // The base UOM is the sole meaning of every quantity recorded against this item —
        // stock layers, ledger rows and document lines are all plain counts of it. Once any
        // of that history exists, changing it would silently reinterpret every one of those
        // numbers (240 bottles becoming 240 cases), so it is frozen instead.
        if (item.BaseUomId != request.BaseUomId && await HasStockHistoryAsync(itemId, ct))
            throw new DomainException(
                ErrorCodes.MasterInUse,
                $"Item {itemId} has stock history; its base UOM can no longer be changed.", 409);

        ItemContent.ValidatePair(request.ContentQty, request.ContentUnit);
        var (contentBaseQty, contentBaseUnit) = ItemContent.Normalize(request.ContentQty, request.ContentUnit);
        var warrantyPeriodMonths = ValidateWarrantyPeriod(request.Warranty, request.WarrantyPeriodMonths);

        item.Sku = request.Sku.Trim();
        item.Name = request.Name.Trim();
        item.Description = request.Description;
        item.CategoryId = request.CategoryId;
        item.SubCategoryId = request.SubCategoryId;
        item.BrandId = request.BrandId;
        item.BaseUomId = request.BaseUomId;
        item.DefaultVendorId = request.DefaultVendorId;
        item.StockNature = request.StockNature;
        item.TrackingMode = request.TrackingMode;
        item.Warranty = request.Warranty;
        item.WarrantyPeriodMonths = warrantyPeriodMonths;
        item.TaxClass = request.TaxClass;
        item.SalePrice = request.SalePrice;
        item.ContentQty = request.ContentQty;
        item.ContentUnit = request.ContentUnit;
        item.ContentBaseQty = contentBaseQty;
        item.ContentBaseUnit = contentBaseUnit;
        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);
    }

    /// <summary>
    /// Whether anything has ever been recorded against this item's stock. Checks the ledger
    /// as well as live layers, because a fully consumed item has no layer left but its
    /// history still reads in the old unit.
    /// </summary>
    private async Task<bool> HasStockHistoryAsync(int itemId, CancellationToken ct)
        => await _stockLayers.Query().AnyAsync(l => l.ItemId == itemId, ct)
           || await _stockLedger.Query().AnyAsync(l => l.ItemId == itemId, ct);

    /// <summary>
    /// Validates every FK on an item write, and gates the optional ones on the product
    /// configuration (FR-MD-11). Note there is no item-type check: nothing on an item
    /// references an item type, so <c>itemTypesEnabled</c> has nothing to reject here —
    /// it is advisory and honoured by the frontend only (docs/11 §2.8).
    /// </summary>
    private async Task ValidateReferencesAsync(
        int categoryId, int? subCategoryId, int? brandId, int baseUomId, int? defaultVendorId, CancellationToken ct)
    {
        var config = (await _config.GetAsync(ct)).Value;

        if (!await _categories.Query().AnyAsync(c => c.CategoryId == categoryId, ct))
            throw new DomainException(ErrorCodes.Validation, $"Category {categoryId} does not exist.", 422);

        if (subCategoryId is not null)
        {
            if (!config.SubcategoriesEnabled)
                throw new DomainException(
                    ErrorCodes.ConfigDisabled,
                    "Subcategories are disabled in the product configuration; subCategoryId must be null.", 422);

            var sub = await _subCategories.Query().AsNoTracking()
                .FirstOrDefaultAsync(s => s.SubCategoryId == subCategoryId, ct);
            if (sub is null)
                throw new DomainException(ErrorCodes.Validation, $"Subcategory {subCategoryId} does not exist.", 422);
            if (sub.Status != EntityStatus.Active)
                throw new DomainException(ErrorCodes.Validation, $"Subcategory {subCategoryId} is inactive.", 422);
            // The two FKs must agree, or the item would claim a category its subcategory
            // does not belong to.
            if (sub.CategoryId != categoryId)
                throw new DomainException(
                    ErrorCodes.Validation,
                    $"Subcategory {subCategoryId} belongs to category {sub.CategoryId}, not {categoryId}.", 422);
        }

        if (brandId is not null)
        {
            if (!config.BrandsEnabled)
                throw new DomainException(
                    ErrorCodes.ConfigDisabled,
                    "Brands are disabled in the product configuration; brandId must be null.", 422);

            var brand = await _brands.Query().AsNoTracking()
                .FirstOrDefaultAsync(b => b.BrandId == brandId, ct);
            if (brand is null)
                throw new DomainException(ErrorCodes.Validation, $"Brand {brandId} does not exist.", 422);
            if (brand.Status != EntityStatus.Active)
                throw new DomainException(ErrorCodes.Validation, $"Brand {brandId} is inactive.", 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);
        }
    }

    /// <summary>
    /// A warranty-tracked item must declare a coverage period (one of
    /// <see cref="WarrantyPeriods.AllowedMonths"/>); a non-warranty item carries none —
    /// any value sent for one is silently dropped rather than trusted from the client.
    /// </summary>
    private static int? ValidateWarrantyPeriod(Warranty warranty, int? warrantyPeriodMonths)
    {
        if (warranty != Warranty.Warranty) return null;

        if (warrantyPeriodMonths is null || !WarrantyPeriods.AllowedMonths.Contains(warrantyPeriodMonths.Value))
            throw new DomainException(
                ErrorCodes.Validation,
                $"Warranty period must be one of {string.Join(", ", WarrantyPeriods.AllowedMonths)} months.", 422);

        return warrantyPeriodMonths;
    }

    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.SubCategoryId, i.BrandId,
        i.BaseUomId, i.DefaultVendorId,
        i.StockNature, i.TrackingMode, i.Warranty, i.WarrantyPeriodMonths, i.TaxClass, i.SalePrice,
        i.ContentQty, i.ContentUnit, i.ContentBaseQty, i.ContentBaseUnit,
        i.Status,
        i.ReorderSettings
            .OrderBy(r => r.WarehouseId)
            .Select(r => new ItemReorderDto(r.WarehouseId, r.ReorderPoint, r.ReorderQty))
            .ToList(),
        i.CreatedAt, i.UpdatedAt);
}
