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

namespace ERPCore.Services;

/// <summary>
/// Category + subcategory master service (FR-MD-04). The hierarchy is exactly two levels:
/// categories no longer self-nest, so there is no cycle to detect and no tree to build
/// (docs/11-BACKEND-PHASE1.md §2.3).
/// </summary>
public sealed class CategoryService : ICategoryService
{
    private readonly IRepository<Category> _categories;
    private readonly IRepository<SubCategory> _subCategories;
    private readonly IUnitOfWork _uow;

    public CategoryService(
        IRepository<Category> categories,
        IRepository<SubCategory> subCategories,
        IUnitOfWork uow)
    {
        _categories = categories;
        _subCategories = subCategories;
        _uow = uow;
    }

    // Categories ---------------------------------------------------------------

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

        var total = await q.CountAsync(ct);
        var rows = await q.OrderBy(c => c.Name)
            .Skip(query.Skip).Take(query.PageSize)
            .Select(c => new CategoryDto(c.CategoryId, c.Name, c.Status, c.CreatedAt, c.UpdatedAt))
            .ToListAsync(ct);

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

    public async Task<ETagged<CategoryDto>?> GetAsync(int categoryId, CancellationToken ct = default)
    {
        var category = await _categories.Query().AsNoTracking()
            .FirstOrDefaultAsync(c => c.CategoryId == categoryId, ct);
        return category is null ? null : new ETagged<CategoryDto>(Map(category), category.RowVersion);
    }

    public async Task<ETagged<CategoryDto>> CreateAsync(CreateCategoryRequest request, CancellationToken ct = default)
    {
        var name = request.Name.Trim();
        if (await _categories.Query().AnyAsync(c => c.Name.ToLower() == name.ToLower(), ct))
            throw new ConflictException($"A category named '{name}' already exists.");

        var category = new Category
        {
            Name = name,
            Status = EntityStatus.Active,
            CreatedAt = DateTime.UtcNow
        };

        await _categories.AddAsync(category, ct);
        await _uow.SaveChangesAsync(ct);

        return new ETagged<CategoryDto>(Map(category), category.RowVersion);
    }

    public async Task<ETagged<CategoryDto>> UpdateAsync(
        int categoryId, UpdateCategoryRequest request, uint expectedRowVersion, CancellationToken ct = default)
    {
        var category = await _categories.GetByIdAsync(categoryId, ct)
            ?? throw new NotFoundException($"Category {categoryId} was not found.");

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

        var name = request.Name.Trim();
        if (!string.Equals(category.Name, name, StringComparison.Ordinal)
            && await _categories.Query().AnyAsync(c => c.Name.ToLower() == name.ToLower() && c.CategoryId != categoryId, ct))
            throw new ConflictException($"A category named '{name}' already exists.");

        category.Name = name;
        category.UpdatedAt = DateTime.UtcNow;

        await SaveGuardingConcurrencyAsync("category", ct);
        return new ETagged<CategoryDto>(Map(category), category.RowVersion);
    }

    public async Task SetStatusAsync(int categoryId, EntityStatus status, CancellationToken ct = default)
    {
        var category = await _categories.GetByIdAsync(categoryId, ct)
            ?? throw new NotFoundException($"Category {categoryId} was not found.");

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

    // Subcategories ------------------------------------------------------------

    public async Task<PagedResponse<SubCategoryDto>> ListSubCategoriesAsync(
        int categoryId, PageQuery query, EntityStatus? status, CancellationToken ct = default)
    {
        if (!await _categories.Query().AnyAsync(c => c.CategoryId == categoryId, ct))
            throw new NotFoundException($"Category {categoryId} was not found.");

        var q = _subCategories.Query().AsNoTracking().Where(s => s.CategoryId == categoryId);
        if (!string.IsNullOrWhiteSpace(query.Q))
        {
            var term = query.Q.Trim();
            q = q.Where(s => EF.Functions.ILike(s.Name, $"%{term}%"));
        }
        if (status is not null) q = q.Where(s => s.Status == status);

        var total = await q.CountAsync(ct);
        var rows = await q.OrderBy(s => s.Name)
            .Skip(query.Skip).Take(query.PageSize)
            .Select(s => new SubCategoryDto(
                s.SubCategoryId, s.CategoryId, s.Name, s.Status, s.CreatedAt, s.UpdatedAt))
            .ToListAsync(ct);

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

    public async Task<ETagged<SubCategoryDto>?> GetSubCategoryAsync(int subCategoryId, CancellationToken ct = default)
    {
        var sub = await _subCategories.Query().AsNoTracking()
            .FirstOrDefaultAsync(s => s.SubCategoryId == subCategoryId, ct);
        return sub is null ? null : new ETagged<SubCategoryDto>(MapSub(sub), sub.RowVersion);
    }

    public async Task<ETagged<SubCategoryDto>> CreateSubCategoryAsync(
        int categoryId, CreateSubCategoryRequest request, CancellationToken ct = default)
    {
        var category = await _categories.Query().AsNoTracking()
            .FirstOrDefaultAsync(c => c.CategoryId == categoryId, ct)
            ?? throw new NotFoundException($"Category {categoryId} was not found.");

        if (category.Status != EntityStatus.Active)
            throw new DomainException(ErrorCodes.Validation, $"Category {categoryId} is inactive.", 422);

        var name = request.Name.Trim();
        if (await _subCategories.Query().AnyAsync(
                s => s.CategoryId == categoryId && s.Name.ToLower() == name.ToLower(), ct))
            throw new ConflictException($"A subcategory named '{name}' already exists under category {categoryId}.");

        var sub = new SubCategory
        {
            CategoryId = categoryId,
            Name = name,
            Status = EntityStatus.Active,
            CreatedAt = DateTime.UtcNow
        };

        await _subCategories.AddAsync(sub, ct);
        await _uow.SaveChangesAsync(ct);

        return new ETagged<SubCategoryDto>(MapSub(sub), sub.RowVersion);
    }

    public async Task<ETagged<SubCategoryDto>> UpdateSubCategoryAsync(
        int subCategoryId, UpdateSubCategoryRequest request, uint expectedRowVersion, CancellationToken ct = default)
    {
        var sub = await _subCategories.GetByIdAsync(subCategoryId, ct)
            ?? throw new NotFoundException($"Subcategory {subCategoryId} was not found.");

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

        var name = request.Name.Trim();
        if (!string.Equals(sub.Name, name, StringComparison.Ordinal)
            && await _subCategories.Query().AnyAsync(
                s => s.CategoryId == sub.CategoryId && s.Name.ToLower() == name.ToLower() && s.SubCategoryId != subCategoryId, ct))
            throw new ConflictException($"A subcategory named '{name}' already exists under category {sub.CategoryId}.");

        // Name only — reparenting is not offered, since it would silently invalidate the
        // category of every item pointing at this subcategory.
        sub.Name = name;
        sub.UpdatedAt = DateTime.UtcNow;

        await SaveGuardingConcurrencyAsync("subcategory", ct);
        return new ETagged<SubCategoryDto>(MapSub(sub), sub.RowVersion);
    }

    public async Task SetSubCategoryStatusAsync(int subCategoryId, EntityStatus status, CancellationToken ct = default)
    {
        var sub = await _subCategories.GetByIdAsync(subCategoryId, ct)
            ?? throw new NotFoundException($"Subcategory {subCategoryId} was not found.");

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

    // ---------------------------------------------------------------------------

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

    private static CategoryDto Map(Category c) => new(c.CategoryId, c.Name, c.Status, c.CreatedAt, c.UpdatedAt);

    private static SubCategoryDto MapSub(SubCategory s) => new(
        s.SubCategoryId, s.CategoryId, s.Name, s.Status, s.CreatedAt, s.UpdatedAt);
}
