using Fitness_core_BE.Exceptions; using Fitness_core_BE.Infra.UoW; using Fitness_core_BE.Interfaces; using Fitness_core_BE.Interfaces.Services; using Fitness_core_BE.Models; using Fitness_core_BE.Models.IOs; using System.Security.Cryptography; namespace Fitness_core_BE.Services { public class AdminClientDirectoryService : IAdminClientDirectoryService { private readonly IAdminClientDirectoryRepository _repository; private readonly IAgentRepo _agentRepo; private readonly IAccessDeviceRepo _accessDeviceRepo; private readonly IPairingCodeRepo _pairingCodeRepo; private readonly IUnitOfWork _UoW; private readonly IAuthService _authService; // Three missed 30-second health heartbeats marks an agent/device offline. // Legacy agents without explicit health reports retain the old grace window. private static readonly TimeSpan HealthOnlineWindow = TimeSpan.FromSeconds(90); private static readonly TimeSpan LegacyOnlineWindow = TimeSpan.FromMinutes(5); public AdminClientDirectoryService( IAdminClientDirectoryRepository repository, IAgentRepo agentRepo, IAccessDeviceRepo accessDeviceRepo, IPairingCodeRepo pairingCodeRepo, IUnitOfWork unitOfWork, IAuthService authService) { _repository = repository; _agentRepo = agentRepo; _accessDeviceRepo = accessDeviceRepo; _pairingCodeRepo = pairingCodeRepo; _UoW = unitOfWork; _authService = authService; } public async Task> ListAsync(ClientDirectoryQuery query) { var (items, total) = await _repository.ListAsync(query.Search, query.Page, query.PageSize); var summaries = items.Select(c => new ClientSummary( c.ClientId, c.ClientName, c.ClientBusinesses.FirstOrDefault()?.City, c.PaymentTrack?.ToString(), c.ClientBusinesses.Any(b => b.IsActive), c.ClientBusinesses.OrderBy(b => b.createdAt).FirstOrDefault()?.createdAt ?? DateTime.MinValue )).ToList(); return new PagedResult(summaries, total, query.Page, query.PageSize); } public async Task GetByIdAsync(Guid id) { var client = await _repository.GetByIdAsync(id) ?? throw new NotFoundException("Client not found."); return new { client.ClientId, client.ClientName, client.ClientPhone, client.ClientEmail, PaymentTrack = client.PaymentTrack?.ToString(), Businesses = client.ClientBusinesses.Select(b => new { b.Bid, b.BusinessName, b.Address, b.City, b.IsActive }) }; } public async Task ResendWelcomeEmailAsync(Guid clientId, HttpContext httpContext) { var client = await _repository.GetByIdAsync(clientId) ?? throw new NotFoundException("Client not found."); if (string.IsNullOrWhiteSpace(client.ClientEmail)) throw new BadRequestException("This client has no email on file to send the link to."); var authResponse = await _authService.ForgotPassword( new { identifier = client.ClientEmail, useResetLink = true, welcome = true }, httpContext); if (authResponse.StatusCode != 200 || !authResponse.Success) throw new BadRequestException(authResponse.Message ?? "Couldn't resend the welcome email."); } public async Task GeneratePairingCodeAsync(Guid clientId) { if (!await _repository.ClientExistsAsync(clientId)) throw new NotFoundException("Client not found."); var pairingCode = new PairingCode { Id = Guid.NewGuid(), Code = GenerateShortCode(), FKClientId = clientId, ExpiresAt = DateTime.UtcNow.AddMinutes(15), IsUsed = false, CreatedAt = DateTime.UtcNow }; await _UoW.BeginAsync(); try { await _pairingCodeRepo.AddAsync(pairingCode); await _UoW.CommitAsync(); } catch { await _UoW.RollbackAsync(); throw; } return new PairingCodeResult(pairingCode.Code, pairingCode.ExpiresAt); } public async Task GetDevicesAsync(Guid clientId) { if (!await _repository.ClientExistsAsync(clientId)) throw new NotFoundException("Client not found."); var agents = await _agentRepo.GetByClientIdAsync(clientId); var devices = await _accessDeviceRepo.GetDevicesByClientAsync(clientId); var now = DateTime.UtcNow; var activeAgentIds = agents .Where(a => a.IsActive) .Select(a => a.AgentId) .ToHashSet(); var agentDtos = agents .Select(agent => new AdminAgentDto( agent.AgentId, agent.Name, agent.IsActive, agent.LastSeenAt, agent.IsActive && agent.LastSeenAt.HasValue && now - agent.LastSeenAt.Value <= HealthOnlineWindow, agent.CreatedAt)) .ToList(); var deviceDtos = devices .Select(device => { var hasHealthReport = device.LastHealthReportedAt.HasValue; var presenceTimestamp = hasHealthReport ? device.LastHealthReportedAt : device.LastSeenAt; var window = hasHealthReport ? HealthOnlineWindow : LegacyOnlineWindow; var isOnline = device.IsActive && device.FKAgentId.HasValue && activeAgentIds.Contains(device.FKAgentId.Value) && presenceTimestamp.HasValue && now - presenceTimestamp.Value <= window && (!hasHealthReport || device.IsReachable); return new AdminDeviceDto( device.DeviceId, device.DeviceName, device.IsActive, device.LastSeenAt, isOnline, isOnline ? presenceTimestamp!.Value.Add(window) : null, device.FKAgentId.HasValue, device.CreatedAt); }) .ToList(); return new AdminClientDevicesResult(agentDtos, deviceDtos); } private static string GenerateShortCode(int length = 8) { const string alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; var bytes = RandomNumberGenerator.GetBytes(length); var chars = new char[length]; for (int i = 0; i < length; i++) chars[i] = alphabet[bytes[i] % alphabet.Length]; return new string(chars); } } }