using Fitness_core_BE.DTOs.Facility; using Fitness_core_BE.Helpers; using Fitness_core_BE.Infra.UoW; using Fitness_core_BE.Interfaces; using Fitness_core_BE.Interfaces.Services; using Fitness_core_BE.Models; using System.Text.Json; namespace Fitness_core_BE.Services { public class FacilityService : IFacilityService { private static readonly string[] ValidBookingModes = ["Scheduled", "AdHoc"]; private readonly IFacilityRepository _facilityRepository; private readonly IUnitOfWork _uow; private readonly UserRoleHelper _userRoleHelper; public FacilityService(IFacilityRepository facilityRepository, IUnitOfWork uow, UserRoleHelper userRoleHelper) { _facilityRepository = facilityRepository; _uow = uow; _userRoleHelper = userRoleHelper; } public async Task GetAllFacilities(object payload, HttpContext httpContext) { var data = DeserializePayload.Deserialize(payload); bool includeInactive = data.ContainsKey("includeInactive") && data["includeInactive"].GetBoolean(); var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext)); var facilities = await _facilityRepository.GetAllFacilities(clientId, includeInactive); var devicesByFacility = await _facilityRepository.GetDevicesByFacilityIdsAsync(facilities.Select(f => f.FacilityId).ToList()); return facilities.Select(f => MapToDto(f, devicesByFacility.GetValueOrDefault(f.FacilityId))).ToList(); } public async Task CreateFacility(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); if (!data.ContainsKey("name")) throw new KeyNotFoundException("name is required"); var bookingMode = data.ContainsKey("bookingMode") ? data["bookingMode"].GetString() ?? "Scheduled" : "Scheduled"; if (!ValidBookingModes.Contains(bookingMode)) throw new ArgumentException($"bookingMode must be one of: {string.Join(", ", ValidBookingModes)}"); var defaultCapacity = data.ContainsKey("defaultCapacity") ? data["defaultCapacity"].GetInt32() : 1; if (bookingMode == "Scheduled" && defaultCapacity < 1) throw new ArgumentException("defaultCapacity must be at least 1"); var numberOfUnits = data.ContainsKey("numberOfUnits") ? data["numberOfUnits"].GetInt32() : 1; if (bookingMode == "AdHoc" && numberOfUnits < 1) throw new ArgumentException("numberOfUnits must be at least 1"); var pricePerHour = data.ContainsKey("pricePerHour") ? data["pricePerHour"].GetDecimal() : 0m; if (pricePerHour < 0) throw new ArgumentException("pricePerHour cannot be negative"); var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext)); var facility = new Facility { FKClientId = clientId, Name = data["name"].GetString() ?? "", BookingMode = bookingMode, Description = data.ContainsKey("description") ? data["description"].GetString() : null, DefaultCapacity = defaultCapacity, NumberOfUnits = numberOfUnits, PricePerHour = pricePerHour }; var result = await _facilityRepository.CreateFacility(facility); await _uow.CommitAsync(); return MapToDto(result, null); } catch { await _uow.RollbackAsync(); throw; } } public async Task UpdateFacility(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); if (!data.ContainsKey("facilityId")) throw new KeyNotFoundException("facilityId is required"); var facilityId = Guid.Parse(data["facilityId"].GetString()!); var existing = await _facilityRepository.GetFacilityById(facilityId) ?? throw new KeyNotFoundException("Facility not found"); if (data.ContainsKey("name")) existing.Name = data["name"].GetString() ?? existing.Name; if (data.ContainsKey("bookingMode")) { var bookingMode = data["bookingMode"].GetString() ?? existing.BookingMode; if (!ValidBookingModes.Contains(bookingMode)) throw new ArgumentException($"bookingMode must be one of: {string.Join(", ", ValidBookingModes)}"); existing.BookingMode = bookingMode; } if (data.ContainsKey("description")) existing.Description = data["description"].GetString(); if (data.ContainsKey("defaultCapacity")) { var capacity = data["defaultCapacity"].GetInt32(); if (capacity < 1) throw new ArgumentException("defaultCapacity must be at least 1"); existing.DefaultCapacity = capacity; } if (data.ContainsKey("numberOfUnits")) { var units = data["numberOfUnits"].GetInt32(); if (units < 1) throw new ArgumentException("numberOfUnits must be at least 1"); existing.NumberOfUnits = units; } if (data.ContainsKey("pricePerHour")) { var price = data["pricePerHour"].GetDecimal(); if (price < 0) throw new ArgumentException("pricePerHour cannot be negative"); existing.PricePerHour = price; } if (data.ContainsKey("isActive")) existing.IsActive = data["isActive"].GetBoolean(); var result = await _facilityRepository.UpdateFacility(existing); await _uow.CommitAsync(); var devicesByFacility = await _facilityRepository.GetDevicesByFacilityIdsAsync(new List { result.FacilityId }); return MapToDto(result, devicesByFacility.GetValueOrDefault(result.FacilityId)); } catch { await _uow.RollbackAsync(); throw; } } public async Task SetFacilityStatus(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); var facilityId = Guid.Parse(data["facilityId"].GetString()!); var isActive = data["isActive"].GetBoolean(); var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext)); var facility = await _facilityRepository.SetFacilityActive(facilityId, clientId, isActive) ?? throw new KeyNotFoundException("Facility not found"); await _uow.CommitAsync(); return MapToDto(facility, null); } catch { await _uow.RollbackAsync(); throw; } } public async Task CreateFacilitySession(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); if (!data.ContainsKey("facilityId")) throw new KeyNotFoundException("facilityId is required"); if (!data.ContainsKey("startTime")) throw new KeyNotFoundException("startTime is required"); if (!data.ContainsKey("endTime")) throw new KeyNotFoundException("endTime is required"); var facilityId = Guid.Parse(data["facilityId"].GetString()!); var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext)); var facility = await _facilityRepository.GetFacilityById(facilityId); if (facility == null || facility.FKClientId != clientId) throw new KeyNotFoundException("Facility not found"); if (facility.BookingMode != "Scheduled") throw new ArgumentException("Sessions can only be created for Scheduled-mode facilities"); var startTime = DateTime.Parse(data["startTime"].GetString()!).ToUniversalTime(); var endTime = DateTime.Parse(data["endTime"].GetString()!).ToUniversalTime(); if (endTime <= startTime) throw new ArgumentException("endTime must be after startTime"); int? capacity = data.ContainsKey("capacity") && data["capacity"].ValueKind != JsonValueKind.Null ? data["capacity"].GetInt32() : null; if (capacity.HasValue && capacity.Value < 1) throw new ArgumentException("capacity must be at least 1 when set"); var session = new FacilitySession { FKFacilityId = facilityId, FKClientId = clientId, StartTime = startTime, EndTime = endTime, Capacity = capacity }; var result = await _facilityRepository.CreateFacilitySession(session); await _uow.CommitAsync(); return MapToDto(result, facility.Name, 0); } catch { await _uow.RollbackAsync(); throw; } } public async Task GetSessions(object payload, HttpContext httpContext) { var data = DeserializePayload.Deserialize(payload); Guid? facilityId = data.ContainsKey("facilityId") && data["facilityId"].ValueKind != JsonValueKind.Null ? Guid.Parse(data["facilityId"].GetString()!) : null; var fromUtc = data.ContainsKey("fromDate") && data["fromDate"].ValueKind != JsonValueKind.Null ? DateTime.Parse(data["fromDate"].GetString()!).ToUniversalTime() : DateTime.UtcNow.Date; var toUtc = data.ContainsKey("toDate") && data["toDate"].ValueKind != JsonValueKind.Null ? DateTime.Parse(data["toDate"].GetString()!).ToUniversalTime() : fromUtc.AddDays(7); var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext)); var sessions = await _facilityRepository.GetSessions(clientId, facilityId, fromUtc, toUtc); var result = new List(); foreach (var session in sessions) { var bookedCount = await _facilityRepository.GetActiveBookingCount(session.FacilitySessionId); result.Add(MapToDto(session, session.Facility?.Name, bookedCount)); } return result; } public async Task CancelFacilitySession(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); var facilitySessionId = Guid.Parse(data["facilitySessionId"].GetString()!); var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext)); var session = await _facilityRepository.CancelFacilitySession(facilitySessionId, clientId) ?? throw new KeyNotFoundException("Facility session not found"); await _uow.CommitAsync(); return MapToDto(session); } catch { await _uow.RollbackAsync(); throw; } } // --- Device binding (absorbed from PlaceService.AssignDevice, unchanged logic) --- public async Task AssignDevice(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); if (!data.ContainsKey("facilityId")) throw new KeyNotFoundException("facilityId is required"); var facilityId = Guid.Parse(data["facilityId"].GetString()!); var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext)); var facility = await _facilityRepository.GetFacilityById(facilityId); if (facility == null || facility.FKClientId != clientId) throw new KeyNotFoundException("Facility not found"); var hasDeviceId = data.ContainsKey("deviceId") && data["deviceId"].ValueKind != JsonValueKind.Null; if (!hasDeviceId) { await _facilityRepository.UnassignDeviceAsync(facilityId); } else { var deviceId = Guid.Parse(data["deviceId"].GetString()!); var device = await _facilityRepository.GetDeviceByIdForClientAsync(deviceId, clientId) ?? throw new KeyNotFoundException("Device not found"); if (await _facilityRepository.IsDeviceAssignedToAnotherFacilityAsync(deviceId, facilityId)) throw new ArgumentException("This device is already assigned to another facility."); await _facilityRepository.AssignDeviceAsync(facilityId, deviceId); } await _uow.CommitAsync(); var devicesByFacility = await _facilityRepository.GetDevicesByFacilityIdsAsync(new List { facilityId }); return MapToDto(facility, devicesByFacility.GetValueOrDefault(facilityId)); } catch { await _uow.RollbackAsync(); throw; } } // --- Availability (new) --- public async Task GetAvailability(object payload, HttpContext httpContext) { var data = DeserializePayload.Deserialize(payload); if (!data.ContainsKey("startTime")) throw new KeyNotFoundException("startTime is required"); if (!data.ContainsKey("durationMinutes")) throw new KeyNotFoundException("durationMinutes is required"); var startTime = DateTime.Parse(data["startTime"].GetString()!).ToUniversalTime(); var durationMinutes = data["durationMinutes"].GetInt32(); if (durationMinutes < 1) throw new ArgumentException("durationMinutes must be at least 1"); var endTime = startTime.AddMinutes(durationMinutes); Guid? facilityId = data.ContainsKey("facilityId") && data["facilityId"].ValueKind != JsonValueKind.Null ? Guid.Parse(data["facilityId"].GetString()!) : null; var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext)); var facilities = facilityId.HasValue ? new List { await _facilityRepository.GetFacilityById(facilityId.Value) is { } f && f.FKClientId == clientId ? f : throw new KeyNotFoundException("Facility not found") } : await _facilityRepository.GetAllFacilities(clientId, includeInactive: false); var result = new List(); foreach (var facility in facilities) { if (facility.BookingMode == "AdHoc") { var overlapping = await _facilityRepository.GetOverlappingAdhocBookingCountAsync(facility.FacilityId, startTime, endTime); result.Add(new FacilityAvailabilityDto { FacilityId = facility.FacilityId, FacilityName = facility.Name, BookingMode = facility.BookingMode, TotalUnits = facility.NumberOfUnits, UnitsFree = Math.Max(0, facility.NumberOfUnits - overlapping), PricePerHour = facility.PricePerHour }); } else { var sessions = await _facilityRepository.GetSessions(clientId, facility.FacilityId, startTime, endTime); var overlappingSessions = sessions.Where(s => s.StartTime < endTime && s.EndTime > startTime).ToList(); var totalCapacity = overlappingSessions.Sum(s => s.Capacity ?? facility.DefaultCapacity); var totalBooked = 0; foreach (var s in overlappingSessions) totalBooked += await _facilityRepository.GetActiveBookingCount(s.FacilitySessionId); result.Add(new FacilityAvailabilityDto { FacilityId = facility.FacilityId, FacilityName = facility.Name, BookingMode = facility.BookingMode, TotalUnits = totalCapacity, UnitsFree = Math.Max(0, totalCapacity - totalBooked), PricePerHour = facility.PricePerHour }); } } return result; } private static FacilityDto MapToDto(Facility facility, AccessDevice? device) => new() { FacilityId = facility.FacilityId, Name = facility.Name, BookingMode = facility.BookingMode, Description = facility.Description, DefaultCapacity = facility.DefaultCapacity, NumberOfUnits = facility.NumberOfUnits, PricePerHour = facility.PricePerHour, IsActive = facility.IsActive, CreatedAt = facility.CreatedAt, DeviceId = device?.DeviceId, DeviceName = device?.DeviceName }; private static FacilitySessionDto MapToDto(FacilitySession session, string? facilityName = null, int bookedCount = 0) => new() { FacilitySessionId = session.FacilitySessionId, FacilityId = session.FKFacilityId, FacilityName = facilityName ?? session.Facility?.Name, StartTime = session.StartTime, EndTime = session.EndTime, Capacity = session.Capacity ?? session.Facility?.DefaultCapacity ?? 1, BookedCount = bookedCount, IsCancelled = session.IsCancelled }; } }