<?php

namespace App\Providers;

use App\Models\ImpersonationToken;
use App\Models\PersonalAccessToken;
use App\Models\User;
use App\Services\Billing\Gateway\PaymentGatewayDriver;
use App\Services\Billing\Gateway\PaymentGatewayManager;
use App\Services\Captcha\CaptchaVerifier;
use App\Services\Captcha\NullCaptchaVerifier;
use App\Services\Captcha\YandexSmartCaptchaVerifier;
use App\Services\DaData\DaDataPartyClient;
use App\Services\DaData\NullPartyLookup;
use App\Services\DaData\PartyLookup;
use App\Services\Supplier\Channel\AjaxProjectChannel;
use App\Services\Supplier\Channel\FailoverProjectChannel;
use App\Services\Supplier\Channel\FormProjectChannel;
use App\Services\Supplier\Channel\SupplierProjectChannel;
use App\Services\Supplier\ProcessFactory;
use App\Services\Supplier\SymfonyProcessFactory;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Laravel\Sanctum\Sanctum;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        $this->app->bind(
            ProcessFactory::class,
            SymfonyProcessFactory::class,
        );

        // Резерв канала миграции проектов: SupplierProjectChannel резолвится в
        // декоратор-оркестратор (ярус 1 AJAX → ярус 2 browser-form → ярус 3 queue).
        // Spec: docs/superpowers/specs/2026-05-19-supplier-project-channel-failover-design.md §4.4
        $this->app->bind(
            SupplierProjectChannel::class,
            fn ($app) => new FailoverProjectChannel(
                $app->make(AjaxProjectChannel::class),
                $app->make(FormProjectChannel::class),
                $app->make(Mailer::class),
            ),
        );

        // Шов капчи самозаписи (G1/SP1 + M-2). Драйвер по CAPTCHA_DRIVER:
        // 'yandex' → реальный Yandex SmartCaptcha; иначе (включая 'null') → Null
        // (dev/test). Контроллер/RegistrationService зовут только интерфейс.
        $this->app->bind(
            CaptchaVerifier::class,
            fn () => match (config('services.captcha.driver')) {
                'yandex' => new YandexSmartCaptchaVerifier,
                default => new NullCaptchaVerifier,
            },
        );

        // Шов подтяжки организации по ИНН (G1/SP2). По флагу party_enabled —
        // реальный DaData suggestions; иначе Null (dev/тесты не ходят в сеть).
        $this->app->bind(
            PartyLookup::class,
            fn ($app) => config('services.dadata.party_enabled')
                ? $app->make(DaDataPartyClient::class)
                : $app->make(NullPartyLookup::class),
        );

        // Шов платёжного драйвера (Task 6, billing-yookassa). Резолвит активный
        // шлюз через PaymentGatewayManager и возвращает конкретный драйвер.
        // Бросает RuntimeException если шлюз не настроен (fail-fast в тестах).
        $this->app->bind(PaymentGatewayDriver::class, function ($app) {
            $manager = $app->make(PaymentGatewayManager::class);
            $gateway = $manager->activeGateway();
            if ($gateway === null) {
                throw new \RuntimeException('Нет активного платёжного шлюза');
            }

            return $manager->driverFor($gateway);
        });
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        // FN-RESET (приёмка 22.06.2026): дефолтное Laravel-уведомление ResetPassword
        // строит ссылку через route('password.reset'), которого в SPA нет — роут
        // сброса объявлен только на фронте (Vue Router /reset/:token, name
        // 'reset-password'; backend знает лишь Route::view('/reset','welcome')).
        // Без этого toMail() бросает «Route [password.reset] not defined» → письмо
        // со ссылкой не уходит никому, сброс пароля сломан для всех.
        // Формат ссылки — ровно тот, что разбирает ResetPasswordView.vue:
        // {app.url}/reset/{token}?email={urlencoded}.
        ResetPassword::createUrlUsing(
            fn (User $user, string $token): string => rtrim((string) config('app.url'), '/')
                .'/reset/'.$token
                .'?email='.urlencode($user->getEmailForPasswordReset()),
        );

        // P1 go-live: per-IP route-throttle поверх прикладного per-credential
        // rate-limit в auth-контроллерах. Именованные лимитеры изолируют счётчики
        // login / 2fa / password. Применение — throttle:<name> в routes/web.php.
        // 20/мин — стартовое значение (выше максимума тестовых циклов), снижать по
        // боевому трафику. Runbook: docs/superpowers/runbooks/2026-06-17-auth-throttle-limits.md
        foreach (['auth-login', 'auth-2fa', 'auth-password', 'auth-register'] as $limiterName) {
            RateLimiter::for(
                $limiterName,
                fn (Request $request) => Limit::perMinute(20)->by($request->ip() ?: 'unknown'),
            );
        }

        // apiv1-rate (приёмка 21.06): публичный read-API сделок (/api/v1/deals)
        // прикрыт per-источник лимитом 120/мин ПЕРЕД ApiKeyAuth — режет brute/DoS
        // по ключам и снимает нагрузку bcrypt/DB до аутентификации. Ключ лимитера —
        // сам Bearer-ключ (sha256, «per ключ»); без заголовка — fallback на IP.
        RateLimiter::for('api-v1', function (Request $request) {
            $header = (string) $request->header('Authorization', '');
            $bearer = str_starts_with($header, 'Bearer ')
                ? trim(substr($header, 7))
                : '';
            $by = $bearer !== ''
                ? 'k:'.hash('sha256', $bearer)
                : 'ip:'.($request->ip() ?: 'unknown');

            return Limit::perMinute(120)->by($by);
        });

        // G7-B: заменяем Sanctum PersonalAccessToken на расширение, которое
        // возвращает null для lpimp_-токенов без запроса к personal_access_tokens.
        // Проект использует SPA cookie-auth — таблица personal_access_tokens отсутствует.
        Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);

        // G7-B: кастомный auth-guard «impersonation».
        // По Bearer-токену вида lpimp_<id>_<secret> резолвит первого активного
        // пользователя тенанта из impersonation_tokens.
        // Запросы к БД идут через pgsql_supplier (BYPASSRLS), чтобы не упереться
        // в RLS до SetTenantContext (middleware «tenant» применяется позже).
        Auth::viaRequest('impersonation', function (Request $request) {
            $header = (string) $request->header('Authorization', '');
            if (! str_starts_with($header, 'Bearer lpimp_')) {
                return null;
            }
            $raw = trim(substr($header, 7)); // lpimp_<id>_<secret>
            $parts = explode('_', $raw, 3);
            if (count($parts) !== 3 || $parts[0] !== 'lpimp' || ! ctype_digit($parts[1])) {
                return null;
            }
            $idStr = $parts[1];
            $secret = $parts[2];

            $token = ImpersonationToken::on('pgsql_supplier')->find((int) $idStr);
            if ($token === null || $token->session_token_hash === null || ! $token->isSessionActive()) {
                return null;
            }
            if (! Hash::check($secret, (string) $token->session_token_hash)) {
                return null;
            }

            return User::on('pgsql_supplier')
                ->where('tenant_id', $token->tenant_id)
                ->where('is_active', true)
                ->orderBy('id')
                ->first();
        });
    }
}
