<?php
declare(strict_types=1);

namespace App\Notifications;

use App\Mailer\EmailMessage;
use App\Mailer\MailerFactory;
use App\Settings\SettingsRepository;
use App\Support\FileLogger;
use App\Support\TranslatableException;
use PDO;
use Throwable;

final class NotificationService
{
    private readonly NotificationChannelRegistry $channelRegistry;

    public function __construct(
        private readonly PDO $pdo,
        private readonly SettingsRepository $settings,
        ?NotificationChannelRegistry $channelRegistry = null,
    ) {
        $this->channelRegistry = $channelRegistry ?? new NotificationChannelRegistry();
    }

    public function notifyAdmins(string $event, string $title, string $body = '', ?string $url = null): void
    {
        $event = trim($event);
        if ($event === '') return;
        if (!(bool) $this->settings->get('notifications.' . $event, true)) return;

        $admins = $this->adminRecipients();
        if ((bool) $this->settings->get('notifications.database_enabled', true)) {
            $this->store($admins, $event, $title, $body, $url);
        }
        if ((bool) $this->settings->get('notifications.email_enabled', false) && (bool) $this->settings->get('mail.enabled', false)) {
            $this->sendEmail($admins, $title, $body, $url);
        }
        if ((bool) $this->settings->get('notifications.telegram_enabled', false)) {
            try {
                $this->sendTelegram($title, $body, $url);
            } catch (TranslatableException $exception) {
                (new FileLogger())->warning('Telegram notification delivery failed.', [
                    'translation_key' => $exception->translationKey,
                    'replace' => $exception->replace,
                ]);
            } catch (Throwable $throwable) {
                (new FileLogger())->error('Telegram notification delivery failed.', $throwable);
            }
        }
        foreach ($this->channelRegistry->all() as $channel => $definition) {
            if (!is_array($definition) || !empty($definition['core'])) continue;
            $setting = trim((string) ($definition['enabled_setting'] ?? ''));
            if ($setting === '' || !(bool) $this->settings->get($setting, false)) continue;
            $sent = $this->channelRegistry->sendCustom((string) $channel, [
                'event' => $event,
                'title' => $title,
                'body' => $body,
                'url' => $url,
                'recipient_count' => count($admins),
            ]);
            if (!$sent) (new FileLogger())->warning('Custom notification channel delivery failed.', ['channel' => (string) $channel]);
        }
    }

    /** @param list<int> $userIds @param list<int> $excludeUserIds */
    public function notifyUsers(array $userIds, string $event, string $title, string $body = '', ?string $url = null, array $excludeUserIds = []): void
    {
        $ids = array_values(array_unique(array_filter(array_map('intval', $userIds), static fn (int $id): bool => $id > 0)));
        $exclude = array_fill_keys(array_map('intval', $excludeUserIds), true);
        $ids = array_values(array_filter($ids, static fn (int $id): bool => !isset($exclude[$id])));
        if ($ids === []) return;

        $placeholders = implode(',', array_fill(0, count($ids), '?'));
        $statement = $this->pdo->prepare("SELECT id,email FROM users WHERE status='active' AND id IN ({$placeholders})");
        $statement->execute($ids);
        $recipients = [];
        foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $id = (int) ($row['id'] ?? 0);
            if ($id <= 0) continue;
            $recipients[$id] = ['id' => $id, 'email' => trim((string) ($row['email'] ?? ''))];
        }

        foreach ($recipients as $recipient) {
            $preference = $this->userPreference((int) $recipient['id'], $event);
            if ($preference['in_app']) {
                $this->store([$recipient], $event, $title, $body, $url);
            }
            if ($preference['email'] && (bool) $this->settings->get('mail.enabled', false)) {
                $this->sendEmail([$recipient], $title, $body, $url, false);
            }
        }
    }

    public function notifyTaxonomyFollowers(int $questionId, string $notificationTitle, ?string $url = null): void
    {
        if ($questionId <= 0) return;
        try {
            $questionStatement = $this->pdo->prepare("SELECT id,user_id,category_id,title,slug FROM questions WHERE id=:id AND status='published' LIMIT 1");
            $questionStatement->execute(['id' => $questionId]);
            $question = $questionStatement->fetch(PDO::FETCH_ASSOC);
            if (!is_array($question)) return;

            $ids = [];
            $categoryId = (int) ($question['category_id'] ?? 0);
            if ($categoryId > 0) {
                $categoryFollowers = $this->pdo->prepare('SELECT user_id FROM category_follows WHERE category_id=:id');
                $categoryFollowers->execute(['id' => $categoryId]);
                $ids = array_merge($ids, array_map('intval', $categoryFollowers->fetchAll(PDO::FETCH_COLUMN)));
            }

            $tagFollowers = $this->pdo->prepare(
                'SELECT DISTINCT tf.user_id FROM tag_follows tf INNER JOIN question_tags qt ON qt.tag_id=tf.tag_id WHERE qt.question_id=:question_id'
            );
            $tagFollowers->execute(['question_id' => $questionId]);
            $ids = array_merge($ids, array_map('intval', $tagFollowers->fetchAll(PDO::FETCH_COLUMN)));

            $this->notifyUsers(
                $ids,
                'followed_question',
                $notificationTitle,
                (string) ($question['title'] ?? ''),
                $url,
                [(int) ($question['user_id'] ?? 0)]
            );
        } catch (Throwable) {
            // A failed follower notification must never roll back question publication.
        }
    }

    /** @return array{in_app:bool,email:bool} */
    private function userPreference(int $userId, string $event): array
    {
        try {
            $statement = $this->pdo->prepare('SELECT in_app_enabled,email_enabled FROM notification_preferences WHERE user_id=:user_id AND event_type=:event LIMIT 1');
            $statement->execute(['user_id' => $userId, 'event' => substr(trim($event), 0, 64)]);
            $row = $statement->fetch(PDO::FETCH_ASSOC);
            if (is_array($row)) {
                return ['in_app' => !empty($row['in_app_enabled']), 'email' => !empty($row['email_enabled'])];
            }
        } catch (Throwable) {
            // Migration 015 may not be installed yet; preserve safe in-app defaults.
        }
        return ['in_app' => true, 'email' => false];
    }

    /** @return list<array<string,mixed>> */
    public function inbox(int $userId, int $limit = 100): array
    {
        $limit = max(1, min(200, $limit));
        $statement = $this->pdo->prepare(
            'SELECT id,type,title,body,url,read_at,created_at FROM notifications WHERE user_id=:user_id ORDER BY created_at DESC,id DESC LIMIT ' . $limit
        );
        $statement->execute(['user_id' => $userId]);
        return $statement->fetchAll(PDO::FETCH_ASSOC);
    }

    public function unreadCount(int $userId): int
    {
        $statement = $this->pdo->prepare('SELECT COUNT(*) FROM notifications WHERE user_id=:user_id AND read_at IS NULL');
        $statement->execute(['user_id' => $userId]);
        return (int) $statement->fetchColumn();
    }

    public function markRead(int $userId, int $notificationId): bool
    {
        $statement = $this->pdo->prepare('UPDATE notifications SET read_at=COALESCE(read_at,NOW()) WHERE id=:id AND user_id=:user_id');
        $statement->execute(['id' => $notificationId, 'user_id' => $userId]);
        return $statement->rowCount() > 0;
    }

    /** @return array<string,array{in_app:bool,email:bool}> */
    public function preferences(int $userId, array $events = ['new_answer', 'new_comment', 'answer_accepted', 'followed_question']): array
    {
        $result = [];
        foreach ($events as $event) $result[$event] = $this->userPreference($userId, (string) $event);
        return $result;
    }

    public function savePreference(int $userId, string $event, bool $inApp, bool $email): void
    {
        $event = substr(trim($event), 0, 64);
        if (!in_array($event, ['new_answer', 'new_comment', 'answer_accepted', 'followed_question'], true)) return;
        $statement = $this->pdo->prepare(
            'INSERT INTO notification_preferences (user_id,event_type,in_app_enabled,email_enabled,updated_at) VALUES (:user_id,:event,:in_app,:email,NOW()) '
            . 'ON DUPLICATE KEY UPDATE in_app_enabled=VALUES(in_app_enabled),email_enabled=VALUES(email_enabled),updated_at=NOW()'
        );
        $statement->execute(['user_id' => $userId, 'event' => $event, 'in_app' => $inApp ? 1 : 0, 'email' => $email ? 1 : 0]);
    }

    /** Test one configured delivery channel without requiring its enable toggle. */
    public function testChannel(string $channel, string $title, string $body = ''): bool
    {
        $channel = strtolower(trim($channel));
        $admins = $this->adminRecipients();
        return match ($channel) {
            'database' => $this->store($admins, 'delivery_test', $title, $body, null),
            'email' => $this->sendEmail($admins, $title, $body, null),
            'telegram' => $this->sendTelegram($title, $body, null),
            default => $this->channelRegistry->sendCustom($channel, [
                'event' => 'delivery_test', 'title' => $title, 'body' => $body, 'url' => null,
                'recipient_count' => count($admins),
            ]),
        };
    }

    /** @return list<array{id:int,email:string}> */
    private function adminRecipients(): array
    {
        $statement = $this->pdo->query(
            "SELECT id, email FROM users WHERE status = 'active' AND LOWER(role) IN ('admin','moderator') ORDER BY role DESC, id ASC"
        );
        $rows = $statement->fetchAll(PDO::FETCH_ASSOC);
        $result = [];
        foreach ($rows as $row) {
            $id = (int) ($row['id'] ?? 0);
            if ($id <= 0) continue;
            $result[] = ['id' => $id, 'email' => trim((string) ($row['email'] ?? ''))];
        }
        return $result;
    }

    /** @param list<array{id:int,email:string}> $admins */
    private function store(array $admins, string $event, string $title, string $body, ?string $url): bool
    {
        if ($admins === []) return false;
        try {
            $statement = $this->pdo->prepare(
                'INSERT INTO notifications (user_id, type, title, body, url, created_at) VALUES (:user_id, :type, :title, :body, :url, NOW())'
            );
            foreach ($admins as $admin) {
                $statement->execute([
                    'user_id' => $admin['id'],
                    'type' => substr($event, 0, 64),
                    'title' => function_exists('mb_substr') ? mb_substr(trim($title), 0, 191, 'UTF-8') : substr(trim($title), 0, 191),
                    'body' => trim($body) === '' ? null : trim($body),
                    'url' => $url === null || trim($url) === '' ? null : substr(trim($url), 0, 500),
                ]);
            }
            return true;
        } catch (Throwable) {
            return false;
        }
    }

    /** @param list<array{id:int,email:string}> $admins */
    private function sendEmail(array $admins, string $title, string $body, ?string $url, bool $useConfiguredAdminEmail = true): bool
    {
        $configured = $useConfiguredAdminEmail ? trim((string) $this->settings->get('notifications.admin_email', '')) : '';
        $emails = $configured !== '' ? [$configured] : array_values(array_filter(array_map(static fn(array $r): string => $r['email'], $admins)));
        if ($emails === []) return false;

        $message = trim($body);
        if ($url !== null && trim($url) !== '') {
            $message .= ($message === '' ? '' : "\n\n") . trim($url);
        }
        $mailer = (new MailerFactory($this->settings, trim((string)$this->settings->get('site.name','Quaeriva')) ?: 'Quaeriva'))->make();
        $sent = false;
        foreach (array_unique($emails) as $email) {
            if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) continue;
            try {
                $mailer->send(new EmailMessage($email, $title, $message));
                $sent = true;
            } catch (Throwable $throwable) {
                (new FileLogger())->error('Notification email delivery failed.', $throwable, ['to' => $email]);
            }
        }
        return $sent;
    }

    private function sendTelegram(string $title, string $body, ?string $url): bool
    {
        $token = trim((string) $this->settings->get('notifications.telegram_bot_token', ''));
        $chatId = trim((string) $this->settings->get('notifications.telegram_chat_id', ''));
        if ($token === '') throw new TranslatableException('notifications.telegram_token_missing');
        if (preg_match('/^[0-9]+:[A-Za-z0-9_-]+$/', $token) !== 1) throw new TranslatableException('notifications.telegram_token_invalid');
        if ($chatId === '') throw new TranslatableException('notifications.telegram_chat_missing');
        if (preg_match('/^-?[0-9]+$/', $chatId) !== 1) throw new TranslatableException('notifications.telegram_chat_invalid');
        if (!function_exists('curl_init')) throw new TranslatableException('notifications.telegram_curl_unavailable');

        $text = trim($title);
        if (trim($body) !== '') $text .= "\n\n" . trim($body);
        if ($url !== null && trim($url) !== '') $text .= "\n\n" . trim($url);
        $text = function_exists('mb_substr') ? mb_substr($text, 0, 3900, 'UTF-8') : substr($text, 0, 3900);

        $ch = curl_init('https://api.telegram.org/bot' . $token . '/sendMessage');
        if ($ch === false) throw new TranslatableException('notifications.telegram_curl_unavailable');
        $status = 0;
        try {
            curl_setopt_array($ch, [
                CURLOPT_POST => true,
                CURLOPT_POSTFIELDS => http_build_query(['chat_id' => $chatId, 'text' => $text], '', '&', PHP_QUERY_RFC3986),
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_CONNECTTIMEOUT => 3,
                CURLOPT_TIMEOUT => 5,
                CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
            ]);
            $response = curl_exec($ch);
            $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
            $json = is_string($response) ? json_decode($response, true) : null;
            $description = is_array($json) ? strtolower(trim((string) ($json['description'] ?? ''))) : '';
            if ($status === 401) {
                (new FileLogger())->warning('Telegram API rejected the bot token.', ['status' => $status]);
                throw new TranslatableException('notifications.telegram_token_rejected');
            }
            if ($status === 403 || str_contains($description, 'forbidden')) {
                (new FileLogger())->warning('Telegram API forbade delivery to the chat.', ['status' => $status]);
                throw new TranslatableException('notifications.telegram_forbidden');
            }
            if ($status === 400) {
                (new FileLogger())->warning('Telegram API rejected the chat request.', ['status' => $status]);
                throw new TranslatableException('notifications.telegram_bad_request');
            }
            if (!is_string($response) || $status < 200 || $status >= 300) {
                (new FileLogger())->warning('Telegram API rejected the notification.', ['status' => $status]);
                throw new TranslatableException('notifications.telegram_api_rejected', ['status' => $status]);
            }
            if (!is_array($json) || ($json['ok'] ?? false) !== true) {
                (new FileLogger())->warning('Telegram API rejected the notification.', ['status' => $status]);
                throw new TranslatableException('notifications.telegram_api_rejected', ['status' => $status]);
            }
            return true;
        } catch (TranslatableException $exception) {
            throw $exception;
        } catch (Throwable $throwable) {
            (new FileLogger())->error('Telegram notification transport failed.', $throwable, ['status' => $status]);
            throw new TranslatableException('notifications.telegram_transport', ['status' => $status], '', $throwable);
        } finally {
            curl_close($ch);
        }
    }
}
