<?php

declare(strict_types=1);

// SPDX-FileCopyrightText: Chen Asraf <contact@casraf.dev>
// SPDX-License-Identifier: AGPL-3.0-or-later

namespace OCA\Forum\Service;

use OCA\Forum\Db\PostMapper;
use OCA\Forum\Db\ReadMarkerMapper;
use OCA\Forum\Db\ThreadMapper;
use OCA\Forum\Db\ThreadSubscriptionMapper;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\Notification\IManager as INotificationManager;
use Psr\Log\LoggerInterface;

class NotificationService {
	public function __construct(
		private INotificationManager $notificationManager,
		private ThreadSubscriptionMapper $subscriptionMapper,
		private ThreadMapper $threadMapper,
		private PostMapper $postMapper,
		private ReadMarkerMapper $readMarkerMapper,
		private IURLGenerator $urlGenerator,
		private IUserManager $userManager,
		private LoggerInterface $logger,
	) {
	}

	/**
	 * Notify subscribed users when a new post is added to a thread
	 */
	public function notifyThreadSubscribers(int $threadId, int $postId, string $authorId): void {
		// Get all subscribed users for this thread
		$subscriptions = $this->subscriptionMapper->findByThread($threadId);

		// Get thread information
		try {
			$thread = $this->threadMapper->find($threadId);
		} catch (\Exception $e) {
			$this->logger->warning('Thread not found for notifications', [
				'threadId' => $threadId,
				'error' => $e->getMessage(),
			]);
			return;
		}

		foreach ($subscriptions as $subscription) {
			$userId = $subscription->getUserId();

			// Don't notify the author of the post
			if ($userId === $authorId) {
				continue;
			}

			// Create or update notification (collating multiple posts)
			$this->createOrUpdateNotification($userId, $threadId, $postId, $thread->getTitle(), $thread->getSlug());
		}
	}

	/**
	 * Send a test notification to a user
	 * Useful for testing the notification system
	 */
	public function sendTestNotification(string $userId): void {
		$this->createOrUpdateNotification($userId, 1, 1, 'Test Thread', 'test-thread');
	}

	/**
	 * Create or update a notification for a user about a thread
	 * This allows collating multiple posts into a single notification
	 */
	private function createOrUpdateNotification(string $userId, int $threadId, int $postId, string $threadTitle, string $threadSlug): void {
		// Calculate the number of unread posts
		$postCount = $this->getUnreadPostCount($userId, $threadId, $postId);

		// Mark existing notifications for this thread/user as processed (to update them)
		$existingNotification = $this->notificationManager->createNotification();
		$existingNotification->setApp('forum')
			->setUser($userId)
			->setObject('thread', (string)$threadId)
			->setSubject('new_posts');
		$this->notificationManager->markProcessed($existingNotification);

		// Create new notification with updated post count
		$notification = $this->notificationManager->createNotification();

		// Generate the thread link and icon
		$threadLink = $this->urlGenerator->linkToRouteAbsolute('forum.page.index') . 't/' . $threadSlug;
		$iconPath = $this->urlGenerator->imagePath('forum', 'app-dark.svg');
		$iconUrl = $this->urlGenerator->getAbsoluteURL($iconPath);

		$notification->setApp('forum')
			->setUser($userId)
			->setDateTime(new \DateTime())
			->setObject('thread', (string)$threadId)
			->setSubject('new_posts', [
				'threadId' => $threadId,
				'threadTitle' => $threadTitle,
				'threadSlug' => $threadSlug,
				'lastPostId' => $postId,
				'postCount' => $postCount,
			])
			->setLink($threadLink)
			->setIcon($iconUrl);

		$this->notificationManager->notify($notification);
	}

	/**
	 * Get the count of unread posts for a user in a thread
	 * Uses an efficient DB COUNT query instead of fetching all posts
	 */
	private function getUnreadPostCount(string $userId, int $threadId, int $latestPostId): int {
		try {
			// Get the user's read marker for this thread
			$readMarker = $this->readMarkerMapper->findByUserAndThread($userId, $threadId);
			$lastReadPostId = $readMarker->getLastReadPostId();

			// Count posts after the last read post using DB query
			$unreadCount = $this->postMapper->countUnreadInThread($threadId, $lastReadPostId);

			return max(1, $unreadCount); // At least 1 (the current post)
		} catch (DoesNotExistException $e) {
			// No read marker, count all posts in the thread
			$count = $this->postMapper->countUnreadInThread($threadId, 0);

			return max(1, $count); // At least 1
		}
	}

	/**
	 * Dismiss notifications for a user when they view a thread
	 */
	public function dismissThreadNotifications(string $userId, int $threadId): void {
		$notification = $this->notificationManager->createNotification();
		$notification->setApp('forum')
			->setUser($userId)
			->setObject('thread', (string)$threadId);

		$this->notificationManager->markProcessed($notification);
	}

	/**
	 * Dismiss notifications when read marker catches up
	 */
	public function dismissNotificationsIfRead(string $userId, int $threadId, int $lastReadPostId): void {
		// Get the thread to check the last post
		try {
			$thread = $this->threadMapper->find($threadId);
			$lastPostId = $thread->getLastPostId();

			// If user has read up to or past the last post, dismiss thread notifications
			if ($lastPostId && $lastReadPostId >= $lastPostId) {
				$this->dismissThreadNotifications($userId, $threadId);
			}
		} catch (\Exception $e) {
			// Thread not found or error, just dismiss anyway
			$this->dismissThreadNotifications($userId, $threadId);
		}

		// Also dismiss mention notifications for posts up to lastReadPostId
		$this->dismissMentionNotificationsUpToPost($userId, $threadId, $lastReadPostId);
	}

	/**
	 * Dismiss all mention notifications for a user in a thread up to a specific post ID
	 * Called when the user views posts (read marker is updated)
	 *
	 * @param string $userId The user who was mentioned (notification recipient)
	 * @param int $threadId The thread ID
	 * @param int $lastReadPostId Dismiss notifications for posts up to and including this ID
	 */
	public function dismissMentionNotificationsUpToPost(string $userId, int $threadId, int $lastReadPostId): void {
		try {
			// Get all posts in the thread up to lastReadPostId
			$posts = $this->postMapper->findByThreadId($threadId);

			foreach ($posts as $post) {
				// Only dismiss for posts the user has read
				if ($post->getId() <= $lastReadPostId) {
					// Dismiss mention notification for this post (scoped by author)
					$this->dismissMentionNotification($userId, $post->getId(), $post->getAuthorId());
				}
			}
		} catch (\Exception $e) {
			$this->logger->warning('Failed to dismiss mention notifications up to post', [
				'userId' => $userId,
				'threadId' => $threadId,
				'lastReadPostId' => $lastReadPostId,
				'error' => $e->getMessage(),
			]);
		}
	}

	/**
	 * Extract mentioned user IDs from content
	 * Supports @username and @"username with spaces" formats
	 *
	 * @param string $content The content to parse
	 * @return array<string> Array of valid user IDs that were mentioned
	 */
	public function extractMentions(string $content): array {
		$mentions = [];

		// Pattern to match @"username with spaces" or @username
		$pattern = '/@(?:"([^"]+)"|([a-zA-Z0-9_.-]+))/';

		if (preg_match_all($pattern, $content, $matches, PREG_SET_ORDER)) {
			foreach ($matches as $match) {
				// Get the username - either from quoted format or simple format
				$userId = !empty($match[1]) ? $match[1] : $match[2];

				// Verify the user exists
				if ($this->userManager->userExists($userId)) {
					$mentions[] = $userId;
				}
			}
		}

		return array_unique($mentions);
	}

	/**
	 * Notify users who were mentioned in a post
	 *
	 * @param int $postId The post ID
	 * @param int $threadId The thread ID
	 * @param string $authorId The post author's user ID
	 * @param array<string> $mentionedUserIds Array of user IDs to notify
	 */
	public function notifyMentionedUsers(int $postId, int $threadId, string $authorId, array $mentionedUserIds): void {
		if (empty($mentionedUserIds)) {
			return;
		}

		try {
			$thread = $this->threadMapper->find($threadId);
		} catch (\Exception $e) {
			$this->logger->warning('Thread not found for mention notifications', [
				'threadId' => $threadId,
				'error' => $e->getMessage(),
			]);
			return;
		}

		// Get author display name
		$author = $this->userManager->get($authorId);
		$authorDisplayName = $author ? $author->getDisplayName() : $authorId;

		foreach ($mentionedUserIds as $userId) {
			// Don't notify the author if they mention themselves
			if ($userId === $authorId) {
				continue;
			}

			$this->createMentionNotification(
				$userId,
				$postId,
				$threadId,
				$thread->getTitle(),
				$thread->getSlug(),
				$authorId,
				$authorDisplayName
			);
		}
	}

	/**
	 * Handle mention notifications when a post is edited
	 * Sends notifications to newly mentioned users and removes notifications from users no longer mentioned
	 *
	 * @param int $postId The post ID
	 * @param int $threadId The thread ID
	 * @param string $authorId The post author's user ID
	 * @param string $oldContent The content before editing
	 * @param string $newContent The content after editing
	 */
	public function handleMentionChanges(int $postId, int $threadId, string $authorId, string $oldContent, string $newContent): void {
		$oldMentions = $this->extractMentions($oldContent);
		$newMentions = $this->extractMentions($newContent);

		// Find newly added mentions
		$addedMentions = array_diff($newMentions, $oldMentions);

		// Find removed mentions
		$removedMentions = array_diff($oldMentions, $newMentions);

		// Send notifications for new mentions
		if (!empty($addedMentions)) {
			$this->notifyMentionedUsers($postId, $threadId, $authorId, $addedMentions);
		}

		// Remove notifications for removed mentions
		foreach ($removedMentions as $userId) {
			$this->dismissMentionNotification($userId, $postId, $authorId);
		}
	}

	/**
	 * Create a mention notification for a user
	 *
	 * Object ID format: {authorId}_{postId}
	 * This ensures notifications are scoped per author, so if user1 and user2
	 * both mention userX in different posts, each has their own notification.
	 * Dismissing user1's mention won't affect user2's notification.
	 */
	private function createMentionNotification(
		string $userId,
		int $postId,
		int $threadId,
		string $threadTitle,
		string $threadSlug,
		string $authorId,
		string $authorDisplayName,
	): void {
		// Object ID includes author to scope notifications per mentioning user
		$objectId = $authorId . '_' . $postId;

		// Mark any existing mention notification for this author+post as processed
		$existingNotification = $this->notificationManager->createNotification();
		$existingNotification->setApp('forum')
			->setUser($userId)
			->setObject('post_mention', $objectId)
			->setSubject('mention');
		$this->notificationManager->markProcessed($existingNotification);

		// Create new notification
		$notification = $this->notificationManager->createNotification();

		// Generate the thread link with correct page and post query params
		$threadLink = $this->generatePostLink($threadSlug, $threadId, $postId);
		$iconPath = $this->urlGenerator->imagePath('forum', 'app-dark.svg');
		$iconUrl = $this->urlGenerator->getAbsoluteURL($iconPath);

		$notification->setApp('forum')
			->setUser($userId)
			->setDateTime(new \DateTime())
			->setObject('post_mention', $objectId)
			->setSubject('mention', [
				'postId' => $postId,
				'threadId' => $threadId,
				'threadTitle' => $threadTitle,
				'threadSlug' => $threadSlug,
				'authorId' => $authorId,
				'authorDisplayName' => $authorDisplayName,
			])
			->setLink($threadLink)
			->setIcon($iconUrl);

		$this->notificationManager->notify($notification);
	}

	/**
	 * Generate a deep link to a specific post with correct page number
	 *
	 * @param string $threadSlug Thread slug
	 * @param int $threadId Thread ID
	 * @param int $postId Post ID
	 * @param int $perPage Posts per page (default 20)
	 * @return string Full URL to the post
	 */
	private function generatePostLink(string $threadSlug, int $threadId, int $postId, int $perPage = 20): string {
		$baseUrl = $this->urlGenerator->linkToRouteAbsolute('forum.page.index') . 't/' . $threadSlug;

		try {
			// Check if this is the first post
			$post = $this->postMapper->find($postId);
			if ($post->getIsFirstPost()) {
				// First post is always on page 1, no page param needed
				return $baseUrl . '?post=' . $postId;
			}

			// Calculate the page number for this reply
			$position = $this->postMapper->getReplyPosition($threadId, $postId);
			$page = (int)floor($position / $perPage) + 1;

			return $baseUrl . '?page=' . $page . '&post=' . $postId;
		} catch (\Exception $e) {
			// Fallback to just the post param if we can't calculate the page
			$this->logger->warning('Failed to calculate page for post link', [
				'postId' => $postId,
				'threadId' => $threadId,
				'error' => $e->getMessage(),
			]);
			return $baseUrl . '?post=' . $postId;
		}
	}

	/**
	 * Dismiss a mention notification for a specific user and post
	 *
	 * @param string $userId The user who was mentioned (notification recipient)
	 * @param int $postId The post ID containing the mention
	 * @param string $authorId The author who created the mention (for scoping)
	 */
	public function dismissMentionNotification(string $userId, int $postId, string $authorId): void {
		$objectId = $authorId . '_' . $postId;
		$notification = $this->notificationManager->createNotification();
		$notification->setApp('forum')
			->setUser($userId)
			->setObject('post_mention', $objectId);

		$this->notificationManager->markProcessed($notification);
	}

	/**
	 * Dismiss all mention notifications for a post (all mentioned users)
	 * Used when a post is deleted
	 *
	 * @param int $postId The post ID
	 * @param string $content The post content to extract mentioned users
	 * @param string $authorId The post author ID (for scoping notifications)
	 */
	public function dismissAllMentionNotifications(int $postId, string $content, string $authorId): void {
		$mentionedUsers = $this->extractMentions($content);

		foreach ($mentionedUsers as $userId) {
			$this->dismissMentionNotification($userId, $postId, $authorId);
		}
	}

	/**
	 * Dismiss all mention notifications for all posts in a thread
	 * Used when a thread is deleted
	 *
	 * @param int $threadId The thread ID
	 */
	public function dismissAllThreadMentionNotifications(int $threadId): void {
		// Get all posts in the thread
		$posts = $this->postMapper->findByThreadId($threadId);

		foreach ($posts as $post) {
			$this->dismissAllMentionNotifications($post->getId(), $post->getContent(), $post->getAuthorId());
		}
	}
}
