<?php

namespace MintyPHP\Module\Notifications\Service;

use MintyPHP\Module\Notifications\Repository\NotificationRepository;
use MintyPHP\Repository\Tenant\UserTenantRepositoryInterface;

class NotificationService
{
    private const MESSAGE_META_KEY = '__message';
    private const DEDUPE_WINDOW_MINUTES = 30;

    /** @var array<string, int|true> Type → dedup window in minutes (true = default window). */
    private const DEDUPE_TYPES = [
        'user.created' => true,
        'user.deleted' => true,
        'user.activated' => true,
        'user.deactivated' => true,
        'user.assignment_changed' => true,
        'scheduler.job_failed' => 1440,
    ];

    public function __construct(
        private readonly NotificationRepository $notificationRepository,
        private readonly UserTenantRepositoryInterface $userTenantRepository
    ) {
    }

    /** @return list<array<string, mixed>> */
    public function listForUser(int $userId, ?int $tenantId = null, int $limit = 50): array
    {
        if ($userId <= 0) {
            return [];
        }

        $notifications = $this->notificationRepository->listByUser($userId, $tenantId, $limit, 0);
        return $this->localizeNotifications($notifications);
    }

    public function unreadCount(int $userId, ?int $tenantId = null): int
    {
        if ($userId <= 0) {
            return 0;
        }

        return $this->notificationRepository->countUnreadByUser($userId, $tenantId);
    }

    public function markAsRead(int $userId, int $notificationId, ?int $tenantId = null): bool
    {
        if ($userId <= 0 || $notificationId <= 0) {
            return false;
        }

        return $this->notificationRepository->markRead($notificationId, $userId, $tenantId);
    }

    public function markAllAsRead(int $userId, ?int $tenantId = null): int
    {
        if ($userId <= 0) {
            return 0;
        }

        return $this->notificationRepository->markAllReadByUser($userId, $tenantId);
    }

    public function dismiss(int $userId, int $notificationId, ?int $tenantId = null): bool
    {
        if ($userId <= 0 || $notificationId <= 0) {
            return false;
        }

        return $this->notificationRepository->delete($notificationId, $userId, $tenantId);
    }

    public function createForUser(
        int $recipientUserId,
        ?int $tenantId,
        string $type,
        string $title,
        ?string $body,
        ?string $link,
        array $data
    ): int|false {
        $message = NotificationMessage::plain($type, $title, $body, $link, $data);
        return $this->createFromMessage($recipientUserId, $tenantId, $message);
    }

    /**
     * Fan-out: create notification for all active users in a tenant, optionally excluding actor.
     *
     * @return int Number of notifications created
     */
    public function createForTenantUsers(
        int $tenantId,
        string $type,
        string $title,
        ?string $body,
        ?string $link,
        array $data,
        ?int $excludeUserId
    ): int {
        $message = NotificationMessage::plain($type, $title, $body, $link, $data);
        return $this->createForTenantUsersFromMessage($tenantId, $message, $excludeUserId);
    }

    /**
     * Fan-out: create notification only for tenant users whose ID is in $allowedUserIds.
     *
     * @param array<int, mixed> $allowedUserIds Flipped array (user ID as key) for fast lookup
     * @return int Number of notifications created
     */
    public function createForTenantAdminUsers(
        int $tenantId,
        string $type,
        string $title,
        ?string $body,
        ?string $link,
        array $data,
        ?int $excludeUserId,
        array $allowedUserIds
    ): int {
        $message = NotificationMessage::plain($type, $title, $body, $link, $data);
        return $this->createForTenantAdminUsersFromMessage($tenantId, $message, $excludeUserId, $allowedUserIds);
    }

    public function createFromMessage(int $recipientUserId, ?int $tenantId, NotificationMessage $message): int|false
    {
        $type = trim($message->type());
        $title = trim($message->title());
        if ($recipientUserId <= 0 || $type === '' || $title === '') {
            return false;
        }

        $persistedData = $this->buildPersistedData($message);
        $dedupeMeta = $this->buildDedupeMeta($recipientUserId, $tenantId, $type, $persistedData);

        return $this->notificationRepository->create([
            'recipient_user_id' => $recipientUserId,
            'tenant_id' => $tenantId,
            'type' => $type,
            'title' => $title,
            'body' => $message->body(),
            'link' => $message->link(),
            'data' => $persistedData !== [] ? $persistedData : null,
            'dedupe_fingerprint' => $dedupeMeta['fingerprint'] ?? null,
            'dedupe_bucket' => $dedupeMeta['bucket'] ?? null,
        ]);
    }

    public function createForTenantUsersFromMessage(int $tenantId, NotificationMessage $message, ?int $excludeUserId): int
    {
        if ($tenantId <= 0 || trim($message->type()) === '' || trim($message->title()) === '') {
            return 0;
        }

        $userIds = $this->userTenantRepository->listActiveUserIdsByTenantId($tenantId);
        $created = 0;

        foreach ($userIds as $userId) {
            if ($excludeUserId !== null && $userId === $excludeUserId) {
                continue;
            }

            $result = $this->createFromMessage($userId, $tenantId, $message);
            if ($result !== false) {
                $created++;
            }
        }

        return $created;
    }

    /**
     * Fan-out using a typed message contract for tenant users filtered by allowed user IDs.
     *
     * @param array<int, mixed> $allowedUserIds Flipped array (user ID as key) for fast lookup
     */
    public function createForTenantAdminUsersFromMessage(
        int $tenantId,
        NotificationMessage $message,
        ?int $excludeUserId,
        array $allowedUserIds
    ): int {
        if ($tenantId <= 0 || trim($message->type()) === '' || trim($message->title()) === '' || $allowedUserIds === []) {
            return 0;
        }

        $userIds = $this->userTenantRepository->listActiveUserIdsByTenantId($tenantId);
        $created = 0;

        foreach ($userIds as $userId) {
            if (!isset($allowedUserIds[$userId])) {
                continue;
            }
            if ($excludeUserId !== null && $userId === $excludeUserId) {
                continue;
            }

            $result = $this->createFromMessage($userId, $tenantId, $message);
            if ($result !== false) {
                $created++;
            }
        }

        return $created;
    }

    public function purgeOldRead(int $retentionDays = 90): int
    {
        return $this->notificationRepository->purgeReadOlderThanDays($retentionDays);
    }

    public function deleteAllForUser(int $userId): int
    {
        if ($userId <= 0) {
            return 0;
        }

        return $this->notificationRepository->deleteAllByUser($userId);
    }

    /**
     * @param array<string, mixed> $data
     * @return array{fingerprint: string, bucket: int}|array{}
     */
    private function buildDedupeMeta(int $recipientUserId, ?int $tenantId, string $type, array $data): array
    {
        if (!isset(self::DEDUPE_TYPES[$type])) {
            return [];
        }

        $target = $this->dedupeTargetFromData($data);
        if ($target === '') {
            return [];
        }

        $windowMinutes = self::DEDUPE_TYPES[$type];
        $windowMinutes = is_int($windowMinutes) ? $windowMinutes : self::DEDUPE_WINDOW_MINUTES;

        $tenantScope = $tenantId !== null && $tenantId > 0 ? $tenantId : 0;
        $raw = implode('|', [
            'recipient:' . $recipientUserId,
            'tenant:' . $tenantScope,
            'type:' . $type,
            'target:' . $target,
        ]);

        return [
            'fingerprint' => hash('sha256', $raw),
            'bucket' => intdiv(time(), $windowMinutes * 60),
        ];
    }

    /**
     * @param array<string, mixed> $data
     */
    private function dedupeTargetFromData(array $data): string
    {
        $explicit = trim((string) ($data['dedupe_target'] ?? ''));
        if ($explicit !== '') {
            return $explicit;
        }

        $targetUserId = (int) ($data['user_id'] ?? 0);
        if ($targetUserId > 0) {
            return 'user:' . $targetUserId;
        }

        $targetUuid = trim((string) ($data['uuid'] ?? ''));
        if ($targetUuid !== '') {
            return 'uuid:' . $targetUuid;
        }

        return '';
    }

    /**
     * @param list<array<string, mixed>> $notifications
     * @return list<array<string, mixed>>
     */
    private function localizeNotifications(array $notifications): array
    {
        $localized = [];
        foreach ($notifications as $notification) {
            $localized[] = $this->localizeNotification($notification);
        }

        return $localized;
    }

    /**
     * @param array<string, mixed> $notification
     * @return array<string, mixed>
     */
    private function localizeNotification(array $notification): array
    {
        $data = $this->decodeNotificationData($notification['data'] ?? null);
        $messageMeta = $this->extractMessageMeta($data);

        if ($messageMeta !== []) {
            if ($messageMeta['title_key'] !== '') {
                $notification['title'] = $this->translateWithParams(
                    $messageMeta['title_key'],
                    $messageMeta['title_params']
                );
            }

            if ($messageMeta['body_key'] !== '') {
                $notification['body'] = $this->translateWithParams(
                    $messageMeta['body_key'],
                    $messageMeta['body_params']
                );
            }
        }

        if ($data !== []) {
            unset($data[self::MESSAGE_META_KEY]);
            if ($data !== []) {
                $notification['data'] = $data;
            } else {
                unset($notification['data']);
            }
        } else {
            unset($notification['data']);
        }

        return $notification;
    }

    /**
     * @param array<string, mixed> $data
     * @return array{title_key: string, title_params: list<scalar|null>, body_key: string, body_params: list<scalar|null>}|array{}
     */
    private function extractMessageMeta(array $data): array
    {
        $meta = $data[self::MESSAGE_META_KEY] ?? null;
        if (!is_array($meta)) {
            return [];
        }

        $titleKey = trim((string) ($meta['title_key'] ?? ''));
        $bodyKey = trim((string) ($meta['body_key'] ?? ''));
        if ($titleKey === '' && $bodyKey === '') {
            return [];
        }

        return [
            'title_key' => $titleKey,
            'title_params' => $this->normalizeMessageParams($meta['title_params'] ?? []),
            'body_key' => $bodyKey,
            'body_params' => $this->normalizeMessageParams($meta['body_params'] ?? []),
        ];
    }

    /**
     * @param list<scalar|null> $params
     */
    private function translateWithParams(string $key, array $params): string
    {
        return (string) t($key, ...$params);
    }

    /**
     * @return list<scalar|null>
     */
    private function normalizeMessageParams(mixed $params): array
    {
        if (!is_array($params)) {
            return [];
        }

        $normalized = [];
        foreach ($params as $value) {
            if (is_scalar($value) || $value === null) {
                $normalized[] = $value;
            }
        }

        return $normalized;
    }

    /**
     * @return array<string, mixed>
     */
    private function decodeNotificationData(mixed $raw): array
    {
        if (is_array($raw)) {
            return $raw;
        }

        if (!is_string($raw) || trim($raw) === '') {
            return [];
        }

        try {
            $decoded = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
        } catch (\JsonException) {
            return [];
        }

        return is_array($decoded) ? $decoded : [];
    }

    /**
     * @return array<string, mixed>
     */
    private function buildPersistedData(NotificationMessage $message): array
    {
        $data = $message->data();
        $titleKey = trim((string) ($message->titleKey() ?? ''));
        $bodyKey = trim((string) ($message->bodyKey() ?? ''));

        if ($titleKey !== '' || $bodyKey !== '') {
            $data[self::MESSAGE_META_KEY] = [
                'title_key' => $titleKey,
                'title_params' => $this->normalizeMessageParams($message->titleParams()),
                'body_key' => $bodyKey,
                'body_params' => $this->normalizeMessageParams($message->bodyParams()),
            ];
        }

        return $data;
    }

}
