<?php
namespace App\Service;
use App\Entity\ChatMessage;
use App\Entity\User;
use App\Entity\Team;
use App\Entity\Season;
use App\Repository\ChatMessageRepository;
use App\Repository\NotificationRepository;
use App\Entity\Notification;
use Symfony\Component\Security\Core\Security;
class ChatService
{
// Corrected constructor with proper namespaces for Repositories
public function __construct(
private ChatMessageRepository $chatMessageRepository,
private NotificationRepository $notificationRepository,
private \App\Repository\TeamRepository $teamRepository, // Use App\Repository namespace
private \App\Repository\SeasonRepository $seasonRepository, // Use App\Repository namespace
private \App\Repository\UserRepository $userRepository, // Use App\Repository namespace
private Security $security
) {}
public function sendMessage(
string $content,
string $type,
?Team $team = null,
?Season $season = null,
?User $recipient = null,
?string $attachmentUrl = null
): ChatMessage {
$user = $this->security->getUser();
// Validate based on type
if ($type === 'team' && !$team) {
throw new \InvalidArgumentException('Team is required for team messages');
}
if ($type === 'season' && !$season) {
throw new \InvalidArgumentException('Season is required for season messages');
}
if ($type === 'direct' && !$recipient) {
throw new \InvalidArgumentException('Recipient is required for direct messages');
}
// Check recipient's DM preference if this is a direct message
if ($type === 'direct' && $recipient) {
$recipientProfile = $recipient->getProfile();
if ($recipientProfile && !$recipientProfile->isDirectMessagesAllowed()) {
// It's important to use a specific exception type or error code if the frontend
// needs to display a specific message to the sender.
// For now, a generic InvalidArgumentException or a custom one.
throw new \Symfony\Component\Security\Core\Exception\AccessDeniedException('This user is not accepting direct messages.');
}
}
// Create message
$message = new ChatMessage();
$message->setUser($user);
$message->setContent($content);
$message->setType($type);
if ($team) {
$message->setTeam($team);
}
if ($season) {
$message->setSeason($season);
}
if ($recipient) {
$message->setRecipient($recipient);
}
if ($attachmentUrl) {
$message->setAttachmentUrl($attachmentUrl);
}
// Save message
$this->chatMessageRepository->save($message, true);
// Create notifications for recipients
$this->createNotificationsForMessage($message);
return $message;
}
private function createNotificationsForMessage(ChatMessage $message): void
{
$sender = $message->getUser();
if ($message->getType() === 'direct') {
// Create notification for direct message recipient
$this->createNotification(
$message->getRecipient(),
'new_message',
'You received a new message',
[
'messageId' => $message->getId(),
'senderId' => $sender->getId()
]
);
} elseif ($message->getType() === 'team') {
// Create notifications for all team members except sender
$team = $message->getTeam();
if ($team) { // Add null check for team
foreach ($team->getMembers() as $member) {
if ($member && $member->getId() !== $sender->getId()) { // Add null check for member
$this->createNotification(
$member,
'new_team_message',
'New message in team chat',
[
'messageId' => $message->getId(),
'teamId' => $team->getId(),
'senderId' => $sender->getId()
]
);
}
}
}
} elseif ($message->getType() === 'season') {
// Create notifications for all season participants except sender
$season = $message->getSeason();
if ($season) { // Add null check for season
foreach ($season->getTeams() as $team) {
// Add null check for team within season
if ($team) {
foreach ($team->getMembers() as $member) {
// Add null check for member and ensure not sender
if ($member && $member->getId() !== $sender->getId()) {
$this->createNotification(
$member,
'new_season_message',
'New message in season chat',
[
'messageId' => $message->getId(),
'seasonId' => $season->getId(),
'senderId' => $sender->getId()
]
);
}
}
}
}
}
}
// No 'global' case needed here
} // End of createNotificationsForMessage method
private function createNotification(User $user, string $type, string $message, array $data): void
{
$notification = new Notification();
$notification->setUser($user);
$notification->setType($type);
$notification->setContent($message);
$notification->setData($data);
$notification->setIsRead(false);
$this->notificationRepository->save($notification, true);
}
public function getMessages(string $type, $channel, int $limit = 50, int $offset = 0): array
{
if ($type === 'team') {
return $this->chatMessageRepository->findByTeam($channel, $limit, $offset);
} elseif ($type === 'season') {
return $this->chatMessageRepository->findBySeason($channel, $limit, $offset);
} elseif ($type === 'direct') {
$currentUser = $this->security->getUser();
return $this->chatMessageRepository->findDirectMessages($currentUser, $channel, $limit, $offset);
}
throw new \InvalidArgumentException('Invalid chat type');
}
public function getNewMessages(string $type, $channel, \DateTimeInterface $since): array
{
if ($type === 'team') {
return $this->chatMessageRepository->findNewByTeam($channel, $since);
} elseif ($type === 'season') {
return $this->chatMessageRepository->findNewBySeason($channel, $since);
} elseif ($type === 'direct') {
$currentUser = $this->security->getUser();
return $this->chatMessageRepository->findNewDirectMessages($currentUser, $channel, $since);
}
throw new \InvalidArgumentException('Invalid chat type');
}
public function markAsRead(ChatMessage $message): void
{
$message->markAsRead();
$this->chatMessageRepository->save($message, true);
}
/**
* Get all available channels for a user
*/
public function getAvailableChannels(User $user): array
{
// Get teams the user is a member of
$teams = $this->teamRepository->findByUser($user); // Use injected repository
// Get seasons the user is participating in
$seasons = $this->seasonRepository->findByUser($user); // Use injected repository
// Get users the user has recently messaged
$recentChatPartners = $this->chatMessageRepository->findRecentChatPartners($user);
return [
'global' => [
[
'id' => 'global',
'name' => 'Global Chat',
'type' => 'global'
]
],
'teams' => $teams,
'seasons' => $seasons,
'direct' => $recentChatPartners
];
}
/**
* Get messages for a specific channel
*/
public function getMessagesByChannel(string $type, ?int $id, int $limit = 50, int $offset = 0): array
{
if ($type === 'global') {
// For global chat, get messages where the user is messaging themselves
return $this->chatMessageRepository->findGlobalMessages($limit, $offset);
} elseif ($type === 'team') {
$team = $this->teamRepository->find($id); // Use injected repository
if (!$team) {
throw new \InvalidArgumentException('Team not found');
}
return $this->chatMessageRepository->findByTeam($team, $limit, $offset);
} elseif ($type === 'season') {
$season = $this->seasonRepository->find($id); // Use injected repository
if (!$season) {
throw new \InvalidArgumentException('Season not found');
}
return $this->chatMessageRepository->findBySeason($season, $limit, $offset);
} elseif ($type === 'direct') {
$recipient = $this->userRepository->find($id);
if (!$recipient) {
// This would likely cause a 404 or specific error, not a 500, but good to be aware of.
// error_log("[DEBUG] getMessagesByChannel: Recipient not found for ID: " . ($id ?? 'null'));
throw new \InvalidArgumentException('User not found for direct message channel.');
}
$currentUser = $this->security->getUser();
if (!$currentUser) {
// This should ideally be caught earlier, e.g., in the controller.
// error_log("[DEBUG] getMessagesByChannel: Current user not available for direct messages.");
throw new \Symfony\Component\Security\Core\Exception\AccessDeniedException('User not authenticated for direct messages.');
}
// ---- DEBUG: Return before calling findDirectMessages ----
// error_log("[DEBUG] getMessagesByChannel: About to call findDirectMessages for recipient ID: " . $recipient->getId() . " and current user ID: " . $currentUser->getId());
$testMessage = new ChatMessage();
$testMessage->setContent("Debug: About to fetch DMs for user ID " . $recipient->getId());
$testMessage->setType('direct');
// $testMessage->setUser($currentUser); // Keep it simple
// $testMessage->setRecipient($recipient);
return [$testMessage];
// ---- END DEBUG ----
// return $this->chatMessageRepository->findDirectMessages($currentUser, $recipient, $limit, $offset);
}
throw new \InvalidArgumentException('Invalid channel type');
}
/**
* Get unread counts for all channels
*/
public function getUnreadCountsByChannel(User $user): array
{
$counts = [
'global' => $this->chatMessageRepository->countUnreadGlobalMessages($user),
'teams' => [],
'seasons' => [],
'direct' => []
];
// Get teams the user is a member of
$teams = $this->teamRepository->findByUser($user); // Use injected repository
foreach ($teams as $team) {
$counts['teams'][$team->getId()] = $this->chatMessageRepository->countUnreadByUserAndTeam($user, $team);
}
// Get seasons the user is participating in
$seasons = $this->seasonRepository->findByUser($user); // Use injected repository
foreach ($seasons as $season) {
$counts['seasons'][$season->getId()] = $this->chatMessageRepository->countUnreadByUserAndSeason($user, $season);
}
// Get users the user has recently messaged
$recentChatPartners = $this->chatMessageRepository->findRecentChatPartners($user);
foreach ($recentChatPartners as $partner) {
$counts['direct'][$partner->getId()] = $this->chatMessageRepository->countUnreadDirectMessages($user, $partner);
}
return $counts;
}
/**
* Mark all messages in a channel as read
*/
public function markChannelAsRead(string $type, ?int $id): void
{
$currentUser = $this->security->getUser();
if ($type === 'global') {
$this->chatMessageRepository->markAllGlobalMessagesAsRead($currentUser);
} elseif ($type === 'team') {
$team = $this->teamRepository->find($id); // Use injected repository
if (!$team) {
throw new \InvalidArgumentException('Team not found');
}
$this->chatMessageRepository->markAllAsReadForUserInTeam($currentUser, $team);
} elseif ($type === 'season') {
$season = $this->seasonRepository->find($id); // Use injected repository
if (!$season) {
throw new \InvalidArgumentException('Season not found');
}
$this->chatMessageRepository->markAllAsReadForUserInSeason($currentUser, $season);
} elseif ($type === 'direct') {
$recipient = $this->userRepository->find($id); // Use injected repository
if (!$recipient) {
throw new \InvalidArgumentException('User not found');
}
$this->chatMessageRepository->markAllDirectMessagesAsRead($currentUser, $recipient);
} else {
throw new \InvalidArgumentException('Invalid channel type');
}
}
public function getRecentMessagesForWidget(User $user, int $limitPerChannel, int $maxChannels): array
{
$widgetMessages = [];
$channelsProcessed = 0;
$availableChannels = $this->getAvailableChannels($user);
// Prioritize channels: DMs, Teams, Seasons, Global (can be adjusted)
$channelOrder = ['direct', 'teams', 'seasons', 'global'];
foreach ($channelOrder as $channelTypeKey) {
if (!isset($availableChannels[$channelTypeKey])) {
continue;
}
$channelsOfType = $availableChannels[$channelTypeKey];
if (!is_array($channelsOfType)) { // Ensure it's an array
continue;
}
foreach ($channelsOfType as $channelInfo) {
if (!$channelInfo) { // Add null check for $channelInfo
continue;
}
if ($channelsProcessed >= $maxChannels) {
break 2; // Break both loops
}
$channelId = null;
$channelName = 'Unknown Channel';
$actualChannelType = $channelTypeKey; // 'direct', 'teams', 'seasons', 'global'
if ($channelTypeKey === 'global' && isset($channelInfo['id']) && $channelInfo['id'] === 'global') {
$channelId = 'global';
$channelName = $channelInfo['name'] ?? 'Global Chat';
} elseif (is_object($channelInfo) && method_exists($channelInfo, 'getId') && method_exists($channelInfo, 'getName')) {
// For Team and Season entities
$channelId = $actualChannelType . '-' . $channelInfo->getId();
$channelName = $channelInfo->getName();
} elseif ($channelTypeKey === 'direct' && is_object($channelInfo) && method_exists($channelInfo, 'getId') && method_exists($channelInfo, 'getUsername')) {
// For User entities (direct messages)
$channelId = 'direct-' . $channelInfo->getId();
$channelName = $channelInfo->getUsername(); // Or a display name if available
} else {
continue; // Skip if channel info is not as expected
}
// Fetch messages for this channel
$idForGetMessages = null;
if ($actualChannelType === 'global') {
$idForGetMessages = null; // getMessagesByChannel expects null for global's $id
} elseif (is_object($channelInfo) && method_exists($channelInfo, 'getId')) {
$idForGetMessages = $channelInfo->getId();
} else {
// Should not happen if $channelInfo is structured correctly from getAvailableChannels
continue;
}
$messages = $this->getMessagesByChannel(
$actualChannelType,
$idForGetMessages,
$limitPerChannel,
0 // offset
);
if (!empty($messages)) {
$formattedMessages = [];
foreach ($messages as $message) {
if (!$message instanceof ChatMessage) continue;
$sender = $message->getUser();
$formattedMessages[] = [
'id' => $message->getId(),
'senderId' => $sender ? $sender->getId() : null,
'senderName' => $sender ? ($sender->getFullName() ?: $sender->getUsername() ?: 'Unknown User') : 'System',
'content' => substr($message->getContent() ?? '', 0, 100) . (strlen($message->getContent() ?? '') > 100 ? '...' : ''), // Snippet
'createdAt' => $message->getCreatedAt() ? $message->getCreatedAt()->format(\DateTimeInterface::ISO8601) : null,
'channelId' => $channelId, // Composite ID like "team-123" or "direct-456"
'channelType' => $actualChannelType,
'channelName' => $channelName, // For display in widget
];
}
// Use a unique key for the channel in the widget, could be $channelId or $channelName
// For simplicity, using $channelName, but ensure it's unique enough or use $channelId
if (!empty($formattedMessages)) {
// Sort messages by createdAt descending to show newest first for each channel
usort($formattedMessages, function ($a, $b) {
$timeA = $a['createdAt'] ? strtotime($a['createdAt']) : 0; // Treat null as very old
$timeB = $b['createdAt'] ? strtotime($b['createdAt']) : 0; // Treat null as very old
if ($timeA === false) $timeA = 0; // Handle potential false from strtotime
if ($timeB === false) $timeB = 0;
return $timeB - $timeA; // For descending order
});
// Use the unique $channelId as the key for widgetMessages
// The frontend will then iterate over Object.values() or use the channelName from the message payload
$widgetMessages[$channelId] = $formattedMessages;
$channelsProcessed++;
}
}
}
}
return $widgetMessages;
}
}