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

namespace ERPCore.Services;

public sealed class RoleService : IRoleService
{
    private readonly IRepository<Role> _roles;
    private readonly IRepository<RolePermission> _rolePermissions;
    private readonly IRepository<Permission> _permissions;
    private readonly IAuthHexClient _authHex;
    private readonly IUnitOfWork _uow;

    public RoleService(
        IRepository<Role> roles, IRepository<RolePermission> rolePermissions, IRepository<Permission> permissions,
        IAuthHexClient authHex, IUnitOfWork uow)
    {
        _roles = roles;
        _rolePermissions = rolePermissions;
        _permissions = permissions;
        _authHex = authHex;
        _uow = uow;
    }

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

        var total = await q.CountAsync(ct);
        var rows = await q.OrderBy(r => r.Code)
            .Skip(query.Skip).Take(query.PageSize)
            .ToListAsync(ct);

        return PagedResponse<RoleDto>.Create(rows.Select(Map).ToList(), query.Page, query.PageSize, total);
    }

    public async Task<ETagged<RoleDto>?> GetAsync(int roleId, CancellationToken ct = default)
    {
        var role = await _roles.Query().AsNoTracking().FirstOrDefaultAsync(r => r.RoleId == roleId, ct);
        return role is null ? null : new ETagged<RoleDto>(Map(role), role.RowVersion);
    }

    public async Task<ETagged<RoleDto>> CreateAsync(CreateRoleRequest request, CancellationToken ct = default)
    {
        var code = request.Code.Trim();
        if (await _roles.Query().AnyAsync(r => r.Code == code, ct))
            throw new ConflictException($"A role with code '{code}' already exists.");

        var authRole = await _authHex.CreateRoleAsync(
            new CreateAuthHexRoleRequest { Code = code, Name = request.Name.Trim() }, ct);

        var role = new Role
        {
            AuthRoleId = authRole.RoleId,
            Code = code,
            Name = request.Name.Trim(),
            IsSystemRole = authRole.IsSystemRole ?? false,
            Status = EntityStatus.Active,
            CreatedAt = DateTime.UtcNow
        };

        await _roles.AddAsync(role, ct);
        await _uow.SaveChangesAsync(ct);

        return new ETagged<RoleDto>(Map(role), role.RowVersion);
    }

    public async Task<ETagged<RoleDto>> UpdateAsync(
        int roleId, UpdateRoleRequest request, uint expectedRowVersion, CancellationToken ct = default)
    {
        var role = await _roles.GetByIdAsync(roleId, ct)
            ?? throw new NotFoundException($"Role {roleId} was not found.");

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

        var code = request.Code.Trim();
        if (!string.Equals(role.Code, code, StringComparison.Ordinal)
            && await _roles.Query().AnyAsync(r => r.Code == code && r.RoleId != roleId, ct))
            throw new ConflictException($"A role with code '{code}' already exists.");

        await _authHex.UpdateRoleAsync(
            new UpdateAuthHexRoleRequest { RoleId = role.AuthRoleId, Code = code, Name = request.Name.Trim() }, ct);

        role.Code = code;
        role.Name = request.Name.Trim();
        role.UpdatedAt = DateTime.UtcNow;

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

        return new ETagged<RoleDto>(Map(role), role.RowVersion);
    }

    public async Task SetStatusAsync(int roleId, EntityStatus status, CancellationToken ct = default)
    {
        var role = await _roles.GetByIdAsync(roleId, ct)
            ?? throw new NotFoundException($"Role {roleId} was not found.");

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

    public async Task DeleteAsync(int roleId, CancellationToken ct = default)
    {
        var role = await _roles.GetByIdAsync(roleId, ct)
            ?? throw new NotFoundException($"Role {roleId} was not found.");

        try
        {
            await _authHex.DeleteRoleAsync(role.AuthRoleId, ct);
        }
        catch (DomainException ex) when (ex.Message.Contains("ROLE_IN_USE", StringComparison.OrdinalIgnoreCase))
        {
            throw new DomainException(ErrorCodes.RoleInUse, "This role is assigned to one or more users.", 409);
        }

        _roles.Remove(role);
        await _uow.SaveChangesAsync(ct);
    }

    public async Task<RolePermissionsDto> GetPermissionsAsync(int roleId, CancellationToken ct = default)
    {
        _ = await _roles.GetByIdAsync(roleId, ct) ?? throw new NotFoundException($"Role {roleId} was not found.");

        var granted = await _rolePermissions.Query().AsNoTracking()
            .Where(rp => rp.RoleId == roleId)
            .Include(rp => rp.Permission)
            .Select(rp => rp.Permission!)
            .ToListAsync(ct);

        return new RolePermissionsDto(
            roleId,
            granted.Where(p => p.NavItemId is not null).Select(p => p.NavItemId!.Value).ToList(),
            granted.Where(p => p.SubNavItemId is not null).Select(p => p.SubNavItemId!.Value).ToList());
    }

    public async Task<RolePermissionsDto> AssignPermissionsAsync(int roleId, AssignRolePermissionsRequest request, CancellationToken ct = default)
    {
        _ = await _roles.GetByIdAsync(roleId, ct) ?? throw new NotFoundException($"Role {roleId} was not found.");

        var existing = await _rolePermissions.Query().Where(rp => rp.RoleId == roleId).ToListAsync(ct);
        foreach (var rp in existing) _rolePermissions.Remove(rp);

        var navIds = request.NavItemIds.Distinct().ToList();
        var subNavIds = request.SubNavItemIds.Distinct().ToList();

        var permissionIds = await _permissions.Query().AsNoTracking()
            .Where(p => (p.NavItemId != null && navIds.Contains(p.NavItemId.Value))
                     || (p.SubNavItemId != null && subNavIds.Contains(p.SubNavItemId.Value)))
            .Select(p => p.PermissionId)
            .ToListAsync(ct);

        foreach (var permissionId in permissionIds)
            await _rolePermissions.AddAsync(new RolePermission { RoleId = roleId, PermissionId = permissionId }, ct);

        await _uow.SaveChangesAsync(ct);

        return await GetPermissionsAsync(roleId, ct);
    }

    public async Task<MeResponseDto> GetMeAsync(string? roleCode, CancellationToken ct = default)
    {
        if (string.IsNullOrWhiteSpace(roleCode))
            return new MeResponseDto(null, null, Array.Empty<string>());

        // AuthHex mints the RoleCode claim independently of ERPCore's stored casing
        // (e.g. token "ADMIN" vs seeded "Admin"), so match case-insensitively — an
        // identity code differing only by case must not lock the user out of the nav.
        var normalized = roleCode.Trim();
        var role = await _roles.Query().AsNoTracking()
            .FirstOrDefaultAsync(r => r.Code.ToLower() == normalized.ToLower(), ct);
        if (role is null)
            return new MeResponseDto(roleCode, null, Array.Empty<string>());

        var permissions = await _rolePermissions.Query().AsNoTracking()
            .Where(rp => rp.RoleId == role.RoleId)
            .Include(rp => rp.Permission!).ThenInclude(p => p.NavItem)
            .Include(rp => rp.Permission!).ThenInclude(p => p.SubNavItem)
            .Select(rp => rp.Permission!)
            .ToListAsync(ct);

        var navCodes = permissions
            .Select(p => p.NavItem?.Code ?? p.SubNavItem?.Code)
            .Where(code => code is not null)
            .Select(code => code!)
            .Distinct()
            .ToList();

        return new MeResponseDto(role.Code, role.Name, navCodes);
    }

    private static RoleDto Map(Role r) => new(
        r.RoleId, r.Code, r.Name, r.IsSystemRole, r.Status, r.CreatedAt, r.UpdatedAt);
}
