<?php
declare(strict_types=1);

/**
 * /functional/notifications/_lib.php
 * Відповідає за:
 * - dispatch email-сповіщень
 * - антидубль / cooldown / dedupe
 * - логування відправок / пропусків / помилок
 * - підготовку даних користувача / групи / поста / коментаря / діалогу
 * - public API:
 *   - ntf_dispatch_private_message()
 *   - ntf_dispatch_friend_request()
 *   - ntf_dispatch_group_invite()
 *   - ntf_dispatch_group_join_request()
 *   - ntf_dispatch_comment_reply()
 *   - ntf_dispatch_post_comment()
 */

if (!function_exists('ntf_h')) {
    function ntf_h(?string $s): string
    {
        return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
    }
}

if (!function_exists('ntf_table_exists')) {
    function ntf_table_exists(PDO $pdo, string $table): bool
    {
        try {
            $st = $pdo->prepare("
                SELECT 1
                FROM information_schema.tables
                WHERE table_schema = DATABASE()
                  AND table_name = ?
                LIMIT 1
            ");
            $st->execute([$table]);
            return (bool)$st->fetchColumn();
        } catch (Throwable $e) {
            return false;
        }
    }
}

if (!function_exists('ntf_column_exists')) {
    function ntf_column_exists(PDO $pdo, string $table, string $column): bool
    {
        try {
            $st = $pdo->prepare("
                SELECT 1
                FROM information_schema.columns
                WHERE table_schema = DATABASE()
                  AND table_name = ?
                  AND column_name = ?
                LIMIT 1
            ");
            $st->execute([$table, $column]);
            return (bool)$st->fetchColumn();
        } catch (Throwable $e) {
            return false;
        }
    }
}

if (!function_exists('ntf_limit')) {
    function ntf_limit(string $s, int $max): string
    {
        $s = trim(preg_replace('~\s+~u', ' ', $s) ?? $s);
        if ($s === '' || $max <= 0) return '';

        if (function_exists('mb_strlen') && function_exists('mb_substr')) {
            if (mb_strlen($s, 'UTF-8') <= $max) return $s;
            return mb_substr($s, 0, max(1, $max - 1), 'UTF-8') . '…';
        }

        if (strlen($s) <= $max) return $s;
        return substr($s, 0, max(1, $max - 1)) . '…';
    }
}

if (!function_exists('ntf_site_name')) {
    function ntf_site_name(): string
    {
        try {
            if (function_exists('t')) {
                $v = trim((string)t('site'));
                if ($v !== '') return $v;
            }
        } catch (Throwable $e) {
        }

        try {
            if (function_exists('app_config')) {
                $cfg = app_config();
                $v = trim((string)($cfg['app']['site_name'] ?? ''));
                if ($v !== '') return $v;
            }
        } catch (Throwable $e) {
        }

        return 'VMEREGI';
    }
}

if (!function_exists('ntf_abs_url')) {
    function ntf_abs_url(string $path): string
    {
        $path = trim($path);
        if ($path === '') $path = '/';

        if (preg_match('~^https?://~i', $path)) {
            return $path;
        }

        if (function_exists('abs_url')) {
            try {
                return (string)abs_url($path);
            } catch (Throwable $e) {
            }
        }

        $https = false;
        $cfv = (string)($_SERVER['HTTP_CF_VISITOR'] ?? '');
        if ($cfv !== '') {
            $j = json_decode($cfv, true);
            if (is_array($j) && strtolower((string)($j['scheme'] ?? '')) === 'https') {
                $https = true;
            }
        }

        if (!$https) {
            $xfp = strtolower(trim((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')));
            if ($xfp === 'https') $https = true;
        }

        if (!$https) {
            $httpsVal = strtolower(trim((string)($_SERVER['HTTPS'] ?? '')));
            $https = ($httpsVal !== '' && $httpsVal !== 'off' && $httpsVal !== '0');
        }

        $host = trim((string)($_SERVER['HTTP_HOST'] ?? ''));
        if ($host === '') return $path;

        if ($path[0] !== '/') $path = '/' . $path;
        return ($https ? 'https://' : 'http://') . $host . $path;
    }
}

if (!function_exists('ntf_app_setting')) {
    function ntf_app_setting(PDO $pdo, string $key, string $default = ''): string
    {
        if ($key === '' || !ntf_table_exists($pdo, 'app_settings')) {
            return $default;
        }

        try {
            $st = $pdo->prepare("SELECT value_text FROM app_settings WHERE key_name = ? LIMIT 1");
            $st->execute([$key]);
            $value = $st->fetchColumn();
            if ($value === false || $value === null) {
                return $default;
            }
            return (string)$value;
        } catch (Throwable $e) {
            return $default;
        }
    }
}

require_once __DIR__ . '/preferences.php';
require_once __DIR__ . '/templates.php';

$__ntfMailer = dirname(__DIR__, 2) . '/includes/mailer.php';
if (is_file($__ntfMailer)) {
    require_once $__ntfMailer;
}
unset($__ntfMailer);

if (!function_exists('ntf_user_display_name')) {
    function ntf_user_display_name(array $row): string
    {
        $full = trim((string)($row['full_name'] ?? ''));
        if ($full !== '') return $full;

        $fn = trim((string)($row['first_name'] ?? ''));
        $ln = trim((string)($row['last_name'] ?? ''));
        $name = trim($fn . ' ' . $ln);
        if ($name !== '') return $name;

        $un = trim((string)($row['username'] ?? ''));
        if ($un !== '') return '@' . $un;

        return 'User';
    }
}

if (!function_exists('ntf_get_user_brief')) {
    function ntf_get_user_brief(PDO $pdo, int $userId): ?array
    {
        if ($userId <= 0) return null;
        if (!ntf_table_exists($pdo, 'users')) return null;

        try {
            $cols = ['id', 'email', 'lang', 'username'];
            if (ntf_column_exists($pdo, 'users', 'full_name'))  $cols[] = 'full_name';
            if (ntf_column_exists($pdo, 'users', 'first_name')) $cols[] = 'first_name';
            if (ntf_column_exists($pdo, 'users', 'last_name'))  $cols[] = 'last_name';

            $st = $pdo->prepare("SELECT " . implode(',', array_unique($cols)) . " FROM users WHERE id = ? LIMIT 1");
            $st->execute([$userId]);
            $row = $st->fetch(PDO::FETCH_ASSOC) ?: null;
            if (!$row) return null;

            $lang = strtolower(trim((string)($row['lang'] ?? 'uk')));
            if (!in_array($lang, ['uk', 'ru', 'en'], true)) $lang = 'uk';
            $row['lang'] = $lang;

            return $row;
        } catch (Throwable $e) {
            return null;
        }
    }
}

if (!function_exists('ntf_get_group_brief')) {
    function ntf_get_group_brief(PDO $pdo, int $groupId): ?array
    {
        if ($groupId <= 0) return null;
        if (!ntf_table_exists($pdo, 'groups')) return null;

        try {
            $cols = ['id'];
            if (ntf_column_exists($pdo, 'groups', 'name')) $cols[] = 'name';
            if (ntf_column_exists($pdo, 'groups', 'slug')) $cols[] = 'slug';

            $st = $pdo->prepare("SELECT " . implode(',', array_unique($cols)) . " FROM `groups` WHERE id = ? LIMIT 1");
            $st->execute([$groupId]);
            return $st->fetch(PDO::FETCH_ASSOC) ?: null;
        } catch (Throwable $e) {
            return null;
        }
    }
}

if (!function_exists('ntf_get_post_brief')) {
    function ntf_get_post_brief(PDO $pdo, int $postId): ?array
    {
        if ($postId <= 0 || !ntf_table_exists($pdo, 'posts')) return null;

        try {
            $cols = ['id', 'user_id'];
            if (ntf_column_exists($pdo, 'posts', 'group_id')) $cols[] = 'group_id';

            $st = $pdo->prepare("SELECT " . implode(',', array_unique($cols)) . " FROM posts WHERE id = ? LIMIT 1");
            $st->execute([$postId]);
            return $st->fetch(PDO::FETCH_ASSOC) ?: null;
        } catch (Throwable $e) {
            return null;
        }
    }
}

if (!function_exists('ntf_get_comment_brief')) {
    function ntf_get_comment_brief(PDO $pdo, int $commentId): ?array
    {
        if ($commentId <= 0 || !ntf_table_exists($pdo, 'comments')) return null;

        try {
            $cols = ['id', 'post_id', 'user_id', 'parent_id'];
            if (ntf_column_exists($pdo, 'comments', 'body')) $cols[] = 'body';
            if (ntf_column_exists($pdo, 'comments', 'created_at')) $cols[] = 'created_at';

            $st = $pdo->prepare("SELECT " . implode(',', array_unique($cols)) . " FROM comments WHERE id = ? LIMIT 1");
            $st->execute([$commentId]);
            return $st->fetch(PDO::FETCH_ASSOC) ?: null;
        } catch (Throwable $e) {
            return null;
        }
    }
}

if (!function_exists('ntf_group_url')) {
    function ntf_group_url(PDO $pdo, int $groupId): string
    {
        $g = ntf_get_group_brief($pdo, $groupId);
        if (!$g) return ntf_abs_url('/group.php');

        $slug = trim((string)($g['slug'] ?? ''));
        if ($slug !== '') {
            return ntf_abs_url('/functional/group/view.php?g=' . rawurlencode($slug));
        }

        return ntf_abs_url('/functional/group/view.php?id=' . (int)$groupId);
    }
}

if (!function_exists('ntf_dialog_url')) {
    function ntf_dialog_url(int $threadId): string
    {
        if ($threadId > 0) {
            return ntf_abs_url('/messages/dialog.php?t=' . $threadId);
        }
        return ntf_abs_url('/messages/');
    }
}

if (!function_exists('ntf_comment_preview')) {
    function ntf_comment_preview(PDO $pdo, int $commentId, string $lang): string
    {
        $row = ntf_get_comment_brief($pdo, $commentId);
        if (!$row) return '';

        $body = trim((string)($row['body'] ?? ''));
        if ($body !== '') {
            return ntf_limit($body, 180);
        }

        return match ($lang) {
            'ru' => 'В комментарии есть текст или вложения.',
            'en' => 'The comment contains text or attachments.',
            default => 'У коментарі є текст або вкладення.',
        };
    }
}

if (!function_exists('ntf_log_has_dedupe')) {
    function ntf_log_has_dedupe(PDO $pdo, string $dedupeKey): bool
    {
        $dedupeKey = trim($dedupeKey);
        if ($dedupeKey === '') return false;
        if (!ntf_table_exists($pdo, 'notification_email_log')) return false;

        try {
            $st = $pdo->prepare("
                SELECT 1
                FROM notification_email_log
                WHERE dedupe_key = ?
                LIMIT 1
            ");
            $st->execute([$dedupeKey]);
            return (bool)$st->fetchColumn();
        } catch (Throwable $e) {
            return false;
        }
    }
}

if (!function_exists('ntf_log_recent_entity_sent')) {
    function ntf_log_recent_entity_sent(PDO $pdo, int $userId, string $type, string $entityType, int $entityId, int $minutes): bool
    {
        if ($userId <= 0 || $type === '' || $entityType === '' || $entityId <= 0 || $minutes <= 0) {
            return false;
        }
        if (!ntf_table_exists($pdo, 'notification_email_log')) {
            return false;
        }

        try {
            $st = $pdo->prepare("
                SELECT 1
                FROM notification_email_log
                WHERE user_id = ?
                  AND type = ?
                  AND entity_type = ?
                  AND entity_id = ?
                  AND status = 'sent'
                  AND created_at >= (NOW() - INTERVAL ? MINUTE)
                LIMIT 1
            ");
            $st->execute([$userId, $type, $entityType, $entityId, $minutes]);
            return (bool)$st->fetchColumn();
        } catch (Throwable $e) {
            return false;
        }
    }
}

if (!function_exists('ntf_log_event')) {
    function ntf_log_event(PDO $pdo, array $payload): void
    {
        if (!ntf_table_exists($pdo, 'notification_email_log')) return;

        $userId      = (int)($payload['user_id'] ?? 0);
        $actorUserId = (int)($payload['actor_user_id'] ?? 0);
        $type        = trim((string)($payload['type'] ?? ''));
        $entityType  = trim((string)($payload['entity_type'] ?? ''));
        $entityId    = (int)($payload['entity_id'] ?? 0);
        $status      = trim((string)($payload['status'] ?? 'sent'));
        $reason      = trim((string)($payload['reason'] ?? ''));
        $subject     = trim((string)($payload['subject'] ?? ''));
        $dedupeKey   = trim((string)($payload['dedupe_key'] ?? ''));
        $errorText   = trim((string)($payload['error_text'] ?? ''));

        if ($userId <= 0 || $type === '') return;
        if (!in_array($status, ['sent', 'skipped', 'error'], true)) $status = 'error';

        try {
            $st = $pdo->prepare("
                INSERT INTO notification_email_log
                    (user_id, actor_user_id, type, entity_type, entity_id, status, reason, subject, dedupe_key, error_text, created_at)
                VALUES
                    (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
            ");
            $st->execute([
                $userId,
                $actorUserId > 0 ? $actorUserId : null,
                $type,
                $entityType !== '' ? $entityType : null,
                $entityId > 0 ? $entityId : null,
                $status,
                $reason !== '' ? $reason : null,
                $subject !== '' ? $subject : null,
                $dedupeKey !== '' ? $dedupeKey : null,
                $errorText !== '' ? $errorText : null,
            ]);
        } catch (Throwable $e) {
            // duplicate dedupe key or log issues must not break app
        }
    }
}

if (!function_exists('ntf_send_mail_and_log')) {
    function ntf_send_mail_and_log(PDO $pdo, array $recipient, array $actor, array $event, array $mail): bool
    {
        $userId   = (int)($recipient['id'] ?? 0);
        $email    = trim((string)($recipient['email'] ?? ''));
        $toName   = ntf_user_display_name($recipient);
        $subject  = trim((string)($mail['subject'] ?? ''));
        $html     = (string)($mail['html'] ?? '');
        $text     = (string)($mail['text'] ?? '');
        $dedupeKey = trim((string)($event['dedupe_key'] ?? ''));

        if ($dedupeKey !== '' && ntf_log_has_dedupe($pdo, $dedupeKey)) {
            return false;
        }

        if ($userId <= 0 || $email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
            ntf_log_event($pdo, [
                'user_id'       => $userId,
                'actor_user_id' => (int)($actor['id'] ?? 0),
                'type'          => (string)($event['type'] ?? ''),
                'entity_type'   => (string)($event['entity_type'] ?? ''),
                'entity_id'     => (int)($event['entity_id'] ?? 0),
                'status'        => 'skipped',
                'reason'        => 'no_email',
                'subject'       => $subject,
                'dedupe_key'    => $dedupeKey,
            ]);
            return false;
        }

        if (!function_exists('mailer_send')) {
            ntf_log_event($pdo, [
                'user_id'       => $userId,
                'actor_user_id' => (int)($actor['id'] ?? 0),
                'type'          => (string)($event['type'] ?? ''),
                'entity_type'   => (string)($event['entity_type'] ?? ''),
                'entity_id'     => (int)($event['entity_id'] ?? 0),
                'status'        => 'error',
                'reason'        => 'mailer_missing',
                'subject'       => $subject,
                'dedupe_key'    => $dedupeKey,
                'error_text'    => 'mailer_send() is not available',
            ]);
            return false;
        }

        $res = mailer_send($email, $toName, $subject, $html, $text);
        $ok = (bool)($res['ok'] ?? false);

        ntf_log_event($pdo, [
            'user_id'       => $userId,
            'actor_user_id' => (int)($actor['id'] ?? 0),
            'type'          => (string)($event['type'] ?? ''),
            'entity_type'   => (string)($event['entity_type'] ?? ''),
            'entity_id'     => (int)($event['entity_id'] ?? 0),
            'status'        => $ok ? 'sent' : 'error',
            'reason'        => $ok ? 'sent' : 'mailer_error',
            'subject'       => $subject,
            'dedupe_key'    => $dedupeKey,
            'error_text'    => $ok ? '' : (string)($res['error'] ?? 'Unknown mailer error'),
        ]);

        return $ok;
    }
}

if (!function_exists('ntf_private_message_should_send')) {
    function ntf_private_message_should_send(PDO $pdo, int $threadId, int $messageId, int $fromUserId, int $toUserId): bool
    {
        if ($threadId <= 0 || $messageId <= 0 || $fromUserId <= 0 || $toUserId <= 0) return false;
        if (!ntf_table_exists($pdo, 'message_threads') || !ntf_table_exists($pdo, 'message_messages')) return false;

        try {
            $st = $pdo->prepare("
                SELECT user1_id, user2_id, user1_last_read_at, user2_last_read_at
                FROM message_threads
                WHERE id = ?
                LIMIT 1
            ");
            $st->execute([$threadId]);
            $thread = $st->fetch(PDO::FETCH_ASSOC) ?: null;
            if (!$thread) return false;

            $readAt = '';
            if ((int)($thread['user1_id'] ?? 0) === $toUserId) {
                $readAt = trim((string)($thread['user1_last_read_at'] ?? ''));
            } elseif ((int)($thread['user2_id'] ?? 0) === $toUserId) {
                $readAt = trim((string)($thread['user2_last_read_at'] ?? ''));
            } else {
                return false;
            }

            $sql = "
                SELECT COUNT(*)
                FROM message_messages
                WHERE thread_id = ?
                  AND sender_id = ?
                  AND msg_type = 'user'
            ";
            $args = [$threadId, $fromUserId];

            if ($readAt !== '') {
                $sql .= " AND created_at > ? ";
                $args[] = $readAt;
            }

            $sql .= " AND id <= ? ";
            $args[] = $messageId;

            $st2 = $pdo->prepare($sql);
            $st2->execute($args);
            $count = (int)$st2->fetchColumn();

            return ($count === 1);
        } catch (Throwable $e) {
            return false;
        }
    }
}

if (!function_exists('ntf_private_message_preview')) {
    function ntf_private_message_preview(PDO $pdo, int $messageId, string $lang): string
    {
        if ($messageId <= 0 || !ntf_table_exists($pdo, 'message_messages')) return '';

        try {
            $st = $pdo->prepare("
                SELECT body, has_attachments
                FROM message_messages
                WHERE id = ?
                LIMIT 1
            ");
            $st->execute([$messageId]);
            $row = $st->fetch(PDO::FETCH_ASSOC) ?: null;
            if (!$row) return '';

            $body = trim((string)($row['body'] ?? ''));
            if ($body !== '') {
                return ntf_limit($body, 180);
            }

            if ((int)($row['has_attachments'] ?? 0) === 1) {
                return match ($lang) {
                    'ru' => 'В сообщении есть вложения.',
                    'en' => 'The message contains attachments.',
                    default => 'У повідомленні є вкладення.',
                };
            }

            return '';
        } catch (Throwable $e) {
            return '';
        }
    }
}

if (!function_exists('ntf_dispatch_private_message')) {
    function ntf_dispatch_private_message(PDO $pdo, int $messageId, int $threadId, int $fromUserId, int $toUserId): bool
    {
        if ($messageId <= 0 || $threadId <= 0 || $fromUserId <= 0 || $toUserId <= 0) return false;
        if ($fromUserId === $toUserId) return false;

        $recipient = ntf_get_user_brief($pdo, $toUserId);
        $actor     = ntf_get_user_brief($pdo, $fromUserId);

        if (!$recipient || !$actor) return false;

        $prefs = ntf_preferences_get($pdo, $toUserId);
        if (!ntf_preferences_is_enabled($prefs, 'private_message')) {
            ntf_log_event($pdo, [
                'user_id'       => $toUserId,
                'actor_user_id' => $fromUserId,
                'type'          => 'private_message',
                'entity_type'   => 'message',
                'entity_id'     => $messageId,
                'status'        => 'skipped',
                'reason'        => 'disabled',
                'dedupe_key'    => 'pm:' . $messageId,
            ]);
            return false;
        }

        if (!ntf_private_message_should_send($pdo, $threadId, $messageId, $fromUserId, $toUserId)) {
            ntf_log_event($pdo, [
                'user_id'       => $toUserId,
                'actor_user_id' => $fromUserId,
                'type'          => 'private_message',
                'entity_type'   => 'message',
                'entity_id'     => $messageId,
                'status'        => 'skipped',
                'reason'        => 'cooldown_or_unread_exists',
                'dedupe_key'    => 'pm:' . $messageId,
            ]);
            return false;
        }

        $lang = (string)($recipient['lang'] ?? 'uk');
        $mail = ntf_tpl_private_message([
            'lang'          => $lang,
            'site_name'     => ntf_site_name(),
            'actor_display' => ntf_user_display_name($actor),
            'snippet'       => ntf_private_message_preview($pdo, $messageId, $lang),
            'button_url'    => ntf_dialog_url($threadId),
            'pref_url'      => ntf_abs_url('/profile/setting/notifications.php'),
        ]);

        return ntf_send_mail_and_log($pdo, $recipient, $actor, [
            'type'        => 'private_message',
            'entity_type' => 'message',
            'entity_id'   => $messageId,
            'dedupe_key'  => 'pm:' . $messageId,
        ], $mail);
    }
}

if (!function_exists('ntf_dispatch_friend_request')) {
    function ntf_dispatch_friend_request(PDO $pdo, int $fromUserId, int $toUserId, int $requestId = 0): bool
    {
        if ($fromUserId <= 0 || $toUserId <= 0 || $fromUserId === $toUserId) return false;

        $recipient = ntf_get_user_brief($pdo, $toUserId);
        $actor     = ntf_get_user_brief($pdo, $fromUserId);

        if (!$recipient || !$actor) return false;

        $prefs = ntf_preferences_get($pdo, $toUserId);
        if (!ntf_preferences_is_enabled($prefs, 'friend_request')) {
            ntf_log_event($pdo, [
                'user_id'       => $toUserId,
                'actor_user_id' => $fromUserId,
                'type'          => 'friend_request',
                'entity_type'   => 'friend_request',
                'entity_id'     => $requestId,
                'status'        => 'skipped',
                'reason'        => 'disabled',
                'dedupe_key'    => $requestId > 0 ? ('fr:' . $requestId) : null,
            ]);
            return false;
        }

        $mail = ntf_tpl_friend_request([
            'lang'          => (string)($recipient['lang'] ?? 'uk'),
            'site_name'     => ntf_site_name(),
            'actor_display' => ntf_user_display_name($actor),
            'button_url'    => ntf_abs_url('/functional/friends/'),
            'pref_url'      => ntf_abs_url('/profile/setting/notifications.php'),
        ]);

        return ntf_send_mail_and_log($pdo, $recipient, $actor, [
            'type'        => 'friend_request',
            'entity_type' => 'friend_request',
            'entity_id'   => $requestId,
            'dedupe_key'  => $requestId > 0 ? ('fr:' . $requestId) : '',
        ], $mail);
    }
}

if (!function_exists('ntf_dispatch_group_invite')) {
    function ntf_dispatch_group_invite(PDO $pdo, int $fromUserId, int $toUserId, int $groupId): bool
    {
        if ($fromUserId <= 0 || $toUserId <= 0 || $groupId <= 0 || $fromUserId === $toUserId) return false;

        $recipient = ntf_get_user_brief($pdo, $toUserId);
        $actor     = ntf_get_user_brief($pdo, $fromUserId);
        $group     = ntf_get_group_brief($pdo, $groupId);

        if (!$recipient || !$actor) return false;

        $prefs = ntf_preferences_get($pdo, $toUserId);
        if (!ntf_preferences_is_enabled($prefs, 'group_invite')) {
            ntf_log_event($pdo, [
                'user_id'       => $toUserId,
                'actor_user_id' => $fromUserId,
                'type'          => 'group_invite',
                'entity_type'   => 'group',
                'entity_id'     => $groupId,
                'status'        => 'skipped',
                'reason'        => 'disabled',
            ]);
            return false;
        }

        $cooldown = (int)ntf_app_setting($pdo, 'notif_email_group_invite_cooldown_minutes', '180');
        if ($cooldown > 0 && ntf_log_recent_entity_sent($pdo, $toUserId, 'group_invite', 'group', $groupId, $cooldown)) {
            ntf_log_event($pdo, [
                'user_id'       => $toUserId,
                'actor_user_id' => $fromUserId,
                'type'          => 'group_invite',
                'entity_type'   => 'group',
                'entity_id'     => $groupId,
                'status'        => 'skipped',
                'reason'        => 'cooldown',
            ]);
            return false;
        }

        $mail = ntf_tpl_group_invite([
            'lang'          => (string)($recipient['lang'] ?? 'uk'),
            'site_name'     => ntf_site_name(),
            'actor_display' => ntf_user_display_name($actor),
            'group_name'    => trim((string)($group['name'] ?? '')),
            'button_url'    => ntf_group_url($pdo, $groupId),
            'pref_url'      => ntf_abs_url('/profile/setting/notifications.php'),
        ]);

        return ntf_send_mail_and_log($pdo, $recipient, $actor, [
            'type'        => 'group_invite',
            'entity_type' => 'group',
            'entity_id'   => $groupId,
            'dedupe_key'  => '',
        ], $mail);
    }
}

if (!function_exists('ntf_dispatch_group_join_request')) {
    function ntf_dispatch_group_join_request(PDO $pdo, int $fromUserId, int $toUserId, int $groupId, string $dedupeKey = ''): bool
    {
        if ($fromUserId <= 0 || $toUserId <= 0 || $groupId <= 0 || $fromUserId === $toUserId) return false;

        $recipient = ntf_get_user_brief($pdo, $toUserId);
        $actor     = ntf_get_user_brief($pdo, $fromUserId);
        $group     = ntf_get_group_brief($pdo, $groupId);

        if (!$recipient || !$actor) return false;

        $prefs = ntf_preferences_get($pdo, $toUserId);
        if (!ntf_preferences_is_enabled($prefs, 'group_join_request')) {
            ntf_log_event($pdo, [
                'user_id'       => $toUserId,
                'actor_user_id' => $fromUserId,
                'type'          => 'group_join_request',
                'entity_type'   => 'group',
                'entity_id'     => $groupId,
                'status'        => 'skipped',
                'reason'        => 'disabled',
                'dedupe_key'    => $dedupeKey,
            ]);
            return false;
        }

        $mail = ntf_tpl_group_join_request([
            'lang'          => (string)($recipient['lang'] ?? 'uk'),
            'site_name'     => ntf_site_name(),
            'actor_display' => ntf_user_display_name($actor),
            'group_name'    => trim((string)($group['name'] ?? '')),
            'button_url'    => ntf_group_url($pdo, $groupId),
            'pref_url'      => ntf_abs_url('/profile/setting/notifications.php'),
        ]);

        return ntf_send_mail_and_log($pdo, $recipient, $actor, [
            'type'        => 'group_join_request',
            'entity_type' => 'group',
            'entity_id'   => $groupId,
            'dedupe_key'  => $dedupeKey,
        ], $mail);
    }

}

if (!function_exists('ntf_dispatch_comment_reply')) {
    function ntf_dispatch_comment_reply(PDO $pdo, int $commentId, int $parentCommentId, int $fromUserId, int $toUserId, string $buttonUrl = ''): bool
    {
        if ($commentId <= 0 || $parentCommentId <= 0 || $fromUserId <= 0 || $toUserId <= 0) return false;
        if ($fromUserId === $toUserId) return false;

        $recipient = ntf_get_user_brief($pdo, $toUserId);
        $actor     = ntf_get_user_brief($pdo, $fromUserId);

        if (!$recipient || !$actor) return false;

        $prefs = ntf_preferences_get($pdo, $toUserId);
        if (!ntf_preferences_is_enabled($prefs, 'comment_reply')) {
            ntf_log_event($pdo, [
                'user_id'       => $toUserId,
                'actor_user_id' => $fromUserId,
                'type'          => 'comment_reply',
                'entity_type'   => 'comment',
                'entity_id'     => $parentCommentId,
                'status'        => 'skipped',
                'reason'        => 'disabled',
            ]);
            return false;
        }

        $cooldown = (int)ntf_app_setting($pdo, 'notif_email_comment_reply_cooldown_minutes', '30');
        if ($cooldown > 0 && ntf_log_recent_entity_sent($pdo, $toUserId, 'comment_reply', 'comment', $parentCommentId, $cooldown)) {
            ntf_log_event($pdo, [
                'user_id'       => $toUserId,
                'actor_user_id' => $fromUserId,
                'type'          => 'comment_reply',
                'entity_type'   => 'comment',
                'entity_id'     => $parentCommentId,
                'status'        => 'skipped',
                'reason'        => 'cooldown',
            ]);
            return false;
        }

        $lang = (string)($recipient['lang'] ?? 'uk');
        $mail = ntf_tpl_comment_reply([
            'lang'          => $lang,
            'site_name'     => ntf_site_name(),
            'actor_display' => ntf_user_display_name($actor),
            'snippet'       => ntf_comment_preview($pdo, $commentId, $lang),
            'button_url'    => ntf_abs_url($buttonUrl !== '' ? $buttonUrl : '/'),
            'pref_url'      => ntf_abs_url('/profile/setting/notifications.php'),
        ]);

        return ntf_send_mail_and_log($pdo, $recipient, $actor, [
            'type'        => 'comment_reply',
            'entity_type' => 'comment',
            'entity_id'   => $parentCommentId,
            'dedupe_key'  => '',
        ], $mail);
    }
}

if (!function_exists('ntf_dispatch_post_comment')) {
    function ntf_dispatch_post_comment(PDO $pdo, int $commentId, int $postId, int $fromUserId, int $toUserId, string $buttonUrl = ''): bool
    {
        if ($commentId <= 0 || $postId <= 0 || $fromUserId <= 0 || $toUserId <= 0) return false;
        if ($fromUserId === $toUserId) return false;

        $recipient = ntf_get_user_brief($pdo, $toUserId);
        $actor     = ntf_get_user_brief($pdo, $fromUserId);

        if (!$recipient || !$actor) return false;

        $prefs = ntf_preferences_get($pdo, $toUserId);
        if (!ntf_preferences_is_enabled($prefs, 'post_comment')) {
            ntf_log_event($pdo, [
                'user_id'       => $toUserId,
                'actor_user_id' => $fromUserId,
                'type'          => 'post_comment',
                'entity_type'   => 'post',
                'entity_id'     => $postId,
                'status'        => 'skipped',
                'reason'        => 'disabled',
            ]);
            return false;
        }

        $cooldown = (int)ntf_app_setting($pdo, 'notif_email_post_comment_cooldown_minutes', '30');
        if ($cooldown > 0 && ntf_log_recent_entity_sent($pdo, $toUserId, 'post_comment', 'post', $postId, $cooldown)) {
            ntf_log_event($pdo, [
                'user_id'       => $toUserId,
                'actor_user_id' => $fromUserId,
                'type'          => 'post_comment',
                'entity_type'   => 'post',
                'entity_id'     => $postId,
                'status'        => 'skipped',
                'reason'        => 'cooldown',
            ]);
            return false;
        }

        $lang = (string)($recipient['lang'] ?? 'uk');
        $mail = ntf_tpl_post_comment([
            'lang'          => $lang,
            'site_name'     => ntf_site_name(),
            'actor_display' => ntf_user_display_name($actor),
            'snippet'       => ntf_comment_preview($pdo, $commentId, $lang),
            'button_url'    => ntf_abs_url($buttonUrl !== '' ? $buttonUrl : '/'),
            'pref_url'      => ntf_abs_url('/profile/setting/notifications.php'),
        ]);

        return ntf_send_mail_and_log($pdo, $recipient, $actor, [
            'type'        => 'post_comment',
            'entity_type' => 'post',
            'entity_id'   => $postId,
            'dedupe_key'  => '',
        ], $mail);
    }
}