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

namespace ERPCore.Services;

/// <summary>
/// Item type master service (FR-MD-10) — maintains the Color/Size/Material list that
/// <c>GET /item-types</c> serves to the frontend builder's dropdown. Nothing references
/// these rows, so there is no in-use check to make and no cascade to worry about
/// (docs/11-BACKEND-PHASE1.md §2.7, docs/10 Part C.9).
/// </summary>
public sealed class ItemTypeService : IItemTypeService
{
    private readonly IRepository<ItemType> _itemTypes;
    private readonly IUnitOfWork _uow;

    public ItemTypeService(IRepository<ItemType> itemTypes, IUnitOfWork uow)
    {
        _itemTypes = itemTypes;
        _uow = uow;
    }

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

        var total = await q.CountAsync(ct);
        var rows = await q.OrderBy(t => t.Name)
            .Skip(query.Skip).Take(query.PageSize)
            .Select(t => new ItemTypeDto(t.ItemTypeId, t.Name, t.IsMeasurable, t.Status, t.CreatedAt, t.UpdatedAt))
            .ToListAsync(ct);

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

    public async Task<ETagged<ItemTypeDto>?> GetAsync(int itemTypeId, CancellationToken ct = default)
    {
        var itemType = await _itemTypes.Query().AsNoTracking()
            .FirstOrDefaultAsync(t => t.ItemTypeId == itemTypeId, ct);
        return itemType is null ? null : new ETagged<ItemTypeDto>(Map(itemType), itemType.RowVersion);
    }

    public async Task<ETagged<ItemTypeDto>> CreateAsync(CreateItemTypeRequest request, CancellationToken ct = default)
    {
        var name = request.Name.Trim();
        if (await _itemTypes.Query().AnyAsync(t => t.Name.ToLower() == name.ToLower(), ct))
            throw new ConflictException($"An item type named '{name}' already exists.");

        var itemType = new ItemType
        {
            Name = name,
            IsMeasurable = request.IsMeasurable,
            Status = EntityStatus.Active,
            CreatedAt = DateTime.UtcNow
        };

        await _itemTypes.AddAsync(itemType, ct);
        await _uow.SaveChangesAsync(ct);

        return new ETagged<ItemTypeDto>(Map(itemType), itemType.RowVersion);
    }

    public async Task<ETagged<ItemTypeDto>> UpdateAsync(
        int itemTypeId, UpdateItemTypeRequest request, uint expectedRowVersion, CancellationToken ct = default)
    {
        var itemType = await _itemTypes.GetByIdAsync(itemTypeId, ct)
            ?? throw new NotFoundException($"Item type {itemTypeId} was not found.");

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

        var name = request.Name.Trim();
        if (!string.Equals(itemType.Name, name, StringComparison.Ordinal)
            && await _itemTypes.Query().AnyAsync(t => t.Name.ToLower() == name.ToLower() && t.ItemTypeId != itemTypeId, ct))
            throw new ConflictException($"An item type named '{name}' already exists.");

        // Renaming does not touch existing items: their SKUs already encode the values that
        // were chosen, and nothing joins back to this row (docs/10 Part C.9).
        itemType.Name = name;
        // Omitted ⇒ keep what is stored. A plain bool would bind an absent property as false and
        // so let a name-only PUT silently clear the flag on every rename.
        itemType.IsMeasurable = request.IsMeasurable ?? itemType.IsMeasurable;
        itemType.UpdatedAt = DateTime.UtcNow;

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

        return new ETagged<ItemTypeDto>(Map(itemType), itemType.RowVersion);
    }

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

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

    private static ItemTypeDto Map(ItemType t) => new(t.ItemTypeId, t.Name, t.IsMeasurable, t.Status, t.CreatedAt, t.UpdatedAt);
}
