using Collaboration.Domain.Enums;
namespace Collaboration.Domain.Entities;
///
/// Represents a collaboration session — ad-hoc or scheduled.
/// Central aggregate for all co-browse and screen share activity.
///
public class CollabSession
{
public Guid SessionId { get; private set; }
/// Short human-friendly code (e.g. "XK-4291") for quick join.
public string SessionCode { get; private set; } = default!;
public string Title { get; private set; } = default!;
public SessionType SessionType { get; private set; }
public SessionStatus Status { get; private set; }
public SessionMode Mode { get; private set; }
// Context — loosely coupled to any module
public string? ContextModuleCode { get; private set; } // e.g. "AP", "HRMS", "CRM"
public string? ContextRecordId { get; private set; }
public string? ContextRecordLabel { get; private set; } // e.g. "Invoice #4521"
public string? ContextRouteUrl { get; private set; } // Deep link URL
// Scheduling
public DateTime? ScheduledAt { get; private set; }
public int? ScheduledDurationMinutes { get; private set; }
// Lifecycle timestamps
public DateTime? StartedAt { get; private set; }
public DateTime? EndedAt { get; private set; }
public DateTime? LastActivityAt { get; private set; }
public DateTime CreatedAt { get; private set; }
// Ownership
public Guid CreatedBy { get; private set; }
public Guid OrgId { get; private set; }
// Configurable per session (can override org defaults)
public int MaxParticipants { get; private set; }
public bool AllowAnonymousJoin { get; private set; }
/// Flexible JSON blob for future extensibility without schema migrations.
public string? SettingsJson { get; private set; }
// Navigation
public IReadOnlyCollection Participants => _participants.AsReadOnly();
public IReadOnlyCollection Invites => _invites.AsReadOnly();
public IReadOnlyCollection Events => _events.AsReadOnly();
private readonly List _participants = new();
private readonly List _invites = new();
private readonly List _events = new();
private CollabSession() { } // EF constructor
public static CollabSession Create(
string title,
SessionType sessionType,
SessionMode mode,
Guid createdBy,
Guid orgId,
int maxParticipants,
string? contextModuleCode = null,
string? contextRecordId = null,
string? contextRecordLabel = null,
string? contextRouteUrl = null,
DateTime? scheduledAt = null,
int? scheduledDurationMinutes = null)
{
var session = new CollabSession
{
SessionId = Guid.NewGuid(),
SessionCode = GenerateSessionCode(),
Title = title,
SessionType = sessionType,
Mode = mode,
Status = sessionType == SessionType.Scheduled ? SessionStatus.Pending : SessionStatus.Active,
CreatedBy = createdBy,
OrgId = orgId,
MaxParticipants = maxParticipants,
ContextModuleCode = contextModuleCode,
ContextRecordId = contextRecordId,
ContextRecordLabel = contextRecordLabel,
ContextRouteUrl = contextRouteUrl,
ScheduledAt = scheduledAt,
ScheduledDurationMinutes = scheduledDurationMinutes,
AllowAnonymousJoin = false,
CreatedAt = DateTime.UtcNow,
LastActivityAt = DateTime.UtcNow,
StartedAt = sessionType == SessionType.AdHoc ? DateTime.UtcNow : null
};
session.AddEvent(SessionEventType.SessionCreated, createdBy);
if (sessionType == SessionType.AdHoc)
session.AddEvent(SessionEventType.SessionStarted, createdBy);
return session;
}
public void Start(Guid triggeredBy)
{
if (Status != SessionStatus.Pending)
throw new InvalidOperationException($"Cannot start a session with status {Status}.");
Status = SessionStatus.Active;
StartedAt = DateTime.UtcNow;
LastActivityAt = DateTime.UtcNow;
AddEvent(SessionEventType.SessionStarted, triggeredBy);
}
public void Pause(Guid triggeredBy)
{
if (Status != SessionStatus.Active)
throw new InvalidOperationException("Only active sessions can be paused.");
Status = SessionStatus.Paused;
AddEvent(SessionEventType.SessionPaused, triggeredBy);
}
public void Resume(Guid triggeredBy)
{
if (Status != SessionStatus.Paused)
throw new InvalidOperationException("Only paused sessions can be resumed.");
Status = SessionStatus.Active;
LastActivityAt = DateTime.UtcNow;
AddEvent(SessionEventType.SessionResumed, triggeredBy);
}
public void End(Guid triggeredBy, bool isAbandoned = false)
{
if (Status == SessionStatus.Ended || Status == SessionStatus.Abandoned)
return;
Status = isAbandoned ? SessionStatus.Abandoned : SessionStatus.Ended;
EndedAt = DateTime.UtcNow;
AddEvent(isAbandoned ? SessionEventType.SessionAbandoned : SessionEventType.SessionEnded, triggeredBy);
}
public void ChangeMode(SessionMode newMode, Guid triggeredBy)
{
Mode = newMode;
AddEvent(SessionEventType.ModeChanged, triggeredBy, payload: $"{{\"mode\":\"{newMode}\"}}");
}
public void UpdateContext(string? moduleCode, string? recordId, string? recordLabel, string? routeUrl)
{
ContextModuleCode = moduleCode;
ContextRecordId = recordId;
ContextRecordLabel = recordLabel;
ContextRouteUrl = routeUrl;
LastActivityAt = DateTime.UtcNow;
}
public void RecordActivity() => LastActivityAt = DateTime.UtcNow;
internal void AddEvent(SessionEventType eventType, Guid triggeredBy, string? payload = null)
{
_events.Add(CollabSessionEvent.Create(SessionId, eventType, triggeredBy, payload));
}
private static string GenerateSessionCode()
{
// Format: XX-9999 (2 letters + 4 digits)
const string letters = "ABCDEFGHJKLMNPQRSTUVWXYZ"; // Exclude I, O (ambiguous)
var rng = Random.Shared;
return $"{letters[rng.Next(letters.Length)]}{letters[rng.Next(letters.Length)]}-{rng.Next(1000, 9999)}";
}
}