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

namespace ERPCore.Services.Hrm;

/// <summary>
/// Department master service (FR-HR-MD-01) — unlimited self-nesting for a real org
/// chart (unlike the two-level-capped Category); <see cref="EnsureNoCycleAsync"/> is
/// the service-level guard since there is no DB-level constraint for this
/// (docs/12-BACKEND-HRM.md A.1/C.1).
/// </summary>
public sealed class DepartmentService : IDepartmentService
{
    private readonly IRepository<Department> _departments;
    private readonly IUnitOfWork _uow;

    public DepartmentService(IRepository<Department> departments, IUnitOfWork uow)
    {
        _departments = departments;
        _uow = uow;
    }

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

        var total = await q.CountAsync(ct);
        var rows = await q.OrderBy(d => d.Name)
            .Skip(query.Skip).Take(query.PageSize)
            .Select(d => Map(d))
            .ToListAsync(ct);

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

    public async Task<ETagged<DepartmentDto>?> GetAsync(int departmentId, CancellationToken ct = default)
    {
        var dept = await _departments.Query().AsNoTracking().FirstOrDefaultAsync(d => d.DepartmentId == departmentId, ct);
        return dept is null ? null : new ETagged<DepartmentDto>(Map(dept), dept.RowVersion);
    }

    public async Task<ETagged<DepartmentDto>> CreateAsync(CreateDepartmentRequest request, CancellationToken ct = default)
    {
        var code = request.Code.Trim();
        if (await _departments.Query().AnyAsync(d => d.Code.ToLower() == code.ToLower(), ct))
            throw new ConflictException($"A department with code '{code}' already exists.");

        if (request.ParentDepartmentId is not null
            && !await _departments.Query().AnyAsync(d => d.DepartmentId == request.ParentDepartmentId, ct))
            throw new DomainException(ErrorCodes.Validation, $"Parent department {request.ParentDepartmentId} was not found.", 422);

        var dept = new Department
        {
            Code = code,
            Name = request.Name.Trim(),
            ParentDepartmentId = request.ParentDepartmentId,
            HeadEmployeeId = request.HeadEmployeeId,
            BranchId = request.BranchId,
            Status = EntityStatus.Active,
            CreatedAt = DateTime.UtcNow
        };

        await _departments.AddAsync(dept, ct);
        await _uow.SaveChangesAsync(ct);

        return new ETagged<DepartmentDto>(Map(dept), dept.RowVersion);
    }

    public async Task<ETagged<DepartmentDto>> UpdateAsync(int departmentId, UpdateDepartmentRequest request, uint expectedRowVersion, CancellationToken ct = default)
    {
        var dept = await _departments.GetByIdAsync(departmentId, ct)
            ?? throw new NotFoundException($"Department {departmentId} was not found.");

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

        if (request.ParentDepartmentId is not null)
        {
            if (request.ParentDepartmentId == departmentId)
                throw new DomainException(ErrorCodes.DepartmentCycleDetected, "A department cannot be its own parent.", 422);

            if (!await _departments.Query().AnyAsync(d => d.DepartmentId == request.ParentDepartmentId, ct))
                throw new DomainException(ErrorCodes.Validation, $"Parent department {request.ParentDepartmentId} was not found.", 422);

            await EnsureNoCycleAsync(departmentId, request.ParentDepartmentId.Value, ct);
        }

        dept.Name = request.Name.Trim();
        dept.ParentDepartmentId = request.ParentDepartmentId;
        dept.HeadEmployeeId = request.HeadEmployeeId;
        dept.BranchId = request.BranchId;
        dept.UpdatedAt = DateTime.UtcNow;

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

        return new ETagged<DepartmentDto>(Map(dept), dept.RowVersion);
    }

    public async Task SetStatusAsync(int departmentId, EntityStatus status, CancellationToken ct = default)
    {
        var dept = await _departments.GetByIdAsync(departmentId, ct)
            ?? throw new NotFoundException($"Department {departmentId} was not found.");

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

    /// <summary>Walks up from <paramref name="newParentId"/>; throws if it ever reaches <paramref name="departmentId"/>.</summary>
    private async Task EnsureNoCycleAsync(int departmentId, int newParentId, CancellationToken ct)
    {
        var currentId = (int?)newParentId;
        var guard = 0;
        while (currentId is not null && guard++ < 1000)
        {
            if (currentId == departmentId)
                throw new DomainException(ErrorCodes.DepartmentCycleDetected, "Setting this parent would create a department cycle.", 422);

            currentId = await _departments.Query().AsNoTracking()
                .Where(d => d.DepartmentId == currentId)
                .Select(d => d.ParentDepartmentId)
                .FirstOrDefaultAsync(ct);
        }
    }

    private static DepartmentDto Map(Department d) => new(
        d.DepartmentId, d.Code, d.Name, d.ParentDepartmentId, d.HeadEmployeeId, d.BranchId, d.Status, d.CreatedAt, d.UpdatedAt);
}
