<?php

declare(strict_types=1);

namespace App\Services;

use App\Mail\InvoicePaidNotification;
use App\Mail\NewLeadsDigestMail;
use App\Mail\TopupSuccessNotification;
use App\Mail\ZeroBalancePausedMail;
use App\Models\Deal;
use App\Models\InAppNotification;
use App\Models\Project;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Throwable;

/**
 * Центральный диспетчер уведомлений (ТЗ §18.5, schema v8.9 §4 users.notification_preferences).
 *
 * Матрица 8 событий × 3 каналов (inapp / push / email) хранится в
 * `users.notification_preferences` JSONB. По умолчанию (канон — schema.sql §users
 * DEFAULT; G2-B 19.06 флипнул new_lead.email false→true — дайджест по умолчанию ВКЛ):
 *   new_lead         {inapp:true, push:true, email:true}
 *   low_balance      {email:true}
 *   zero_balance     {email:true}
 *   topup_success    {email:true}
 *   invoice_paid     {email:true}
 *   new_device_login {email:true}
 *   marketing        {email:false}
 *
 * Stage 1 (этот коммит): email-канал реализован для new_lead.
 * Stage 2 — in-app (требует in_app_notifications таблицы, schema v8.10).
 * Stage 6 — остальные 4 email-события (low_balance/zero_balance/topup_success/invoice_paid).
 * push-канал — Post-MVP (требует ServiceWorker + VAPID).
 */
class NotificationService
{
    public const EVENT_NEW_LEAD = 'new_lead';

    public const EVENT_LOW_BALANCE = 'low_balance';

    public const EVENT_ZERO_BALANCE = 'zero_balance';

    public const EVENT_TOPUP_SUCCESS = 'topup_success';

    public const EVENT_INVOICE_PAID = 'invoice_paid';

    public const EVENT_NEW_DEVICE_LOGIN = 'new_device_login';

    public const EVENT_MARKETING = 'marketing';

    /** Эпик 6: объяснение правил сбора (хвост источника, вступление правок и т.п.). */
    public const EVENT_PROJECT_RULE = 'project_rule';

    public const ALL_EVENTS = [
        self::EVENT_NEW_LEAD,
        self::EVENT_LOW_BALANCE,
        self::EVENT_ZERO_BALANCE,
        self::EVENT_TOPUP_SUCCESS,
        self::EVENT_INVOICE_PAID,
        self::EVENT_NEW_DEVICE_LOGIN,
        self::EVENT_MARKETING,
    ];

    public const CHANNEL_INAPP = 'inapp';

    public const CHANNEL_PUSH = 'push';

    public const CHANNEL_EMAIL = 'email';

    /**
     * Уведомление о новом лиде — два канала:
     *   email: notification_preferences.new_lead.email=true (раздел 18.5);
     *   inapp: notification_preferences.new_lead.inapp=true (bell-icon).
     *
     * Webhook-поток: вызывается из ProcessWebhookJob::chargeNewLead() после
     * успешного INSERT'а сделки + BalanceTransaction + ActivityLog.
     *
     * Расхождение каналов по schema-default: new_lead = {inapp:true, push:true,
     * email:false} — большинство получит in-app, и только подписавшиеся — email.
     */
    public function notifyNewLead(Tenant $tenant, Deal $deal): void
    {
        $projectName = $deal->project?->name ?? 'Без проекта';

        // Канал email события new_lead переведён на дайджест (SendNewLeadsDigestJob).
        // Здесь — только in-app (колокольчик) на каждую сделку.
        // Канал inapp.
        $title = "Новый лид — {$projectName}";
        $body = $deal->contact_name ?: $deal->phone;
        foreach ($this->recipientsForEvent($tenant, self::EVENT_NEW_LEAD, self::CHANNEL_INAPP) as $user) {
            $this->notifyInApp($user, self::EVENT_NEW_LEAD, $title, $body, [
                'deal_id' => $deal->id,
                'project_name' => $projectName,
            ]);
        }
    }

    /**
     * G2-A дайджест: одно письмо-сводка о новых сделках за окно — каждому
     * активному пользователю тенанта с включённым new_lead.email. Заменяет
     * пер-лид email-канал события new_lead.
     */
    public function notifyNewLeadsDigest(Tenant $tenant, Collection $deals): void
    {
        foreach ($this->recipientsForEvent($tenant, self::EVENT_NEW_LEAD, self::CHANNEL_EMAIL) as $user) {
            $this->sendEmail($user, self::EVENT_NEW_LEAD, new NewLeadsDigestMail($user, $tenant, $deals));
        }
    }

    /**
     * Уведомление об auto-pause проекта на нулевом балансе (Plan 4 Task 6).
     *
     * В отличие от notifyZeroBalance (per-user prefs, rejected-flow), это
     * прямое письмо tenant.contact_email — caller (RouteSupplierLeadJob::
     * handleInsufficientBalance) уже применил rate-limit 1/час/tenant через
     * Cache::store('redis')->add() (SETNX).
     *
     * Spec: docs/superpowers/specs/2026-05-11-plan4-billing-csv-admin-design.md §4.4.
     */
    public function notifyZeroBalancePaused(Tenant $tenant, Project $project, int $requiredPriceKopecks): void
    {
        Mail::to($tenant->contact_email)->send(
            new ZeroBalancePausedMail($tenant, $project, $requiredPriceKopecks)
        );
    }

    /**
     * Уведомление об успешном пополнении. Триггер: после INSERT в
     * balance_transactions с type='topup' (endpoint пополнения отдельным
     * коммитом). Mailable готов к подключению.
     */
    public function notifyTopupSuccess(Tenant $tenant, string $amountRub, ?int $amountLeads = null): void
    {
        $title = "Баланс пополнен: +{$amountRub} ₽";
        $body = $amountLeads !== null ? "+{$amountLeads} лидов" : null;

        foreach ($this->recipientsForEvent($tenant, self::EVENT_TOPUP_SUCCESS, self::CHANNEL_EMAIL) as $user) {
            $this->sendEmail(
                $user,
                self::EVENT_TOPUP_SUCCESS,
                new TopupSuccessNotification($user, $tenant, $amountRub, $amountLeads),
            );
        }
        foreach ($this->recipientsForEvent($tenant, self::EVENT_TOPUP_SUCCESS, self::CHANNEL_INAPP) as $user) {
            $this->notifyInApp($user, self::EVENT_TOPUP_SUCCESS, $title, $body, [
                'tenant_id' => $tenant->id,
                'amount_rub' => $amountRub,
            ]);
        }
    }

    /**
     * Уведомление об оплате тарифного счёта. Триггер: смена
     * saas_invoice.status → 'paid' (webhook ЮKassa отдельным коммитом).
     * Mailable готов к подключению.
     */
    public function notifyInvoicePaid(
        Tenant $tenant,
        string $amountRub,
        ?string $invoiceNumber = null,
        ?string $tariffName = null,
    ): void {
        $title = "Счёт оплачен: {$amountRub} ₽";
        $body = $tariffName !== null ? "Тариф: {$tariffName}" : null;

        foreach ($this->recipientsForEvent($tenant, self::EVENT_INVOICE_PAID, self::CHANNEL_EMAIL) as $user) {
            $this->sendEmail(
                $user,
                self::EVENT_INVOICE_PAID,
                new InvoicePaidNotification($user, $tenant, $amountRub, $invoiceNumber, $tariffName),
            );
        }
        foreach ($this->recipientsForEvent($tenant, self::EVENT_INVOICE_PAID, self::CHANNEL_INAPP) as $user) {
            $this->notifyInApp($user, self::EVENT_INVOICE_PAID, $title, $body, [
                'tenant_id' => $tenant->id,
                'amount_rub' => $amountRub,
                'invoice_number' => $invoiceNumber,
            ]);
        }
    }

    /**
     * Эпик 6: in-app уведомление-объяснение правила сбора всем активным
     * пользователям тенанта (через колокольчик). БЕЗ pref-гейта — правила должны
     * доходить до клиента всегда (это объяснение «как сейчас», не маркетинг).
     * Текст — из ProjectRuleMessages (единый источник). Сбой канала не валит вызов.
     *
     * @param  array<string, mixed>  $payload
     */
    public function notifyProjectRule(Tenant $tenant, string $title, string $body, array $payload = []): void
    {
        $users = User::query()
            ->where('tenant_id', $tenant->id)
            ->where('is_active', true)
            ->whereNull('deleted_at')
            ->get();

        foreach ($users as $user) {
            $this->notifyInApp(
                $user,
                self::EVENT_PROJECT_RULE,
                $title,
                $body,
                array_merge(['tenant_id' => $tenant->id], $payload),
            );
        }
    }

    /**
     * INSERT в `in_app_notifications` для bell-icon UI. RLS требует
     * `app.current_tenant_id` = user.tenant_id, поэтому SET LOCAL внутри
     * мини-транзакции (PgBouncer-safe). Throwable проглатываются + Log:
     * сбой канала не должен валить трансакцию сделки.
     *
     * @param  array<string, mixed>  $payload
     */
    public function notifyInApp(User $user, string $event, string $title, ?string $body = null, array $payload = []): void
    {
        try {
            DB::transaction(function () use ($user, $event, $title, $body, $payload): void {
                DB::statement('SET LOCAL app.current_tenant_id = '.(int) $user->tenant_id);

                InAppNotification::create([
                    'tenant_id' => $user->tenant_id,
                    'user_id' => $user->id,
                    'event' => $event,
                    'title' => $title,
                    'body' => $body,
                    'deal_id' => $payload['deal_id'] ?? null,
                    'payload' => $payload,
                ]);
            });
        } catch (Throwable $e) {
            Log::warning('notification.inapp_failed', [
                'user_id' => $user->id,
                'event' => $event,
                'error' => $e->getMessage(),
            ]);
        }
    }

    /**
     * Активные user'ы тенанта с включённой подпиской на (event, channel).
     * Soft-deleted и is_active=false исключены. Для тестов: маленькие
     * списки получателей (<50 на тенант), фильтр в PHP вместо JSONB-запроса.
     *
     * @return Collection<int, User>
     */
    private function recipientsForEvent(Tenant $tenant, string $event, string $channel): Collection
    {
        return User::query()
            ->where('tenant_id', $tenant->id)
            ->where('is_active', true)
            ->whereNull('deleted_at')
            ->get()
            ->filter(fn (User $user): bool => $this->prefEnabled($user, $event, $channel))
            ->values();
    }

    /**
     * Читает users.notification_preferences[event][channel]. NULL/missing → false.
     */
    private function prefEnabled(User $user, string $event, string $channel): bool
    {
        $prefs = $user->notification_preferences;

        if (! is_array($prefs)) {
            return false;
        }

        return (bool) ($prefs[$event][$channel] ?? false);
    }

    /**
     * Отправка email через Mail-фасад. На dev (MAIL_MAILER=log) пишется в
     * storage/logs/laravel.log, на prod — Unisender Go (SMTP relay).
     *
     * Используется Mail::send (sync) для простоты отладки. Для prod
     * рекомендуется Mail::queue (async через worker), чтобы webhook
     * worker не блокировался на SMTP. Бросаемые исключения проглатываются
     * — отказ канала уведомления не должен валить транзакцию сделки.
     */
    private function sendEmail(User $user, string $event, $mailable): void
    {
        try {
            Mail::to($user->email)->send($mailable);
        } catch (Throwable $e) {
            Log::warning('notification.email_failed', [
                'user_id' => $user->id,
                'event' => $event,
                'error' => $e->getMessage(),
            ]);
        }
    }
}
