<?php

namespace App\Shared\EventSubscriber;

use App\Config\Service\MaintenanceService;
use Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoderInterface;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Twig\Environment;

/**
 * کنترل مرکزی حالت تعمیرات برای همهٔ درخواست‌ها — وب، API و کلاینت‌های خارجی
 * (`nobat724_front`, `clinic-pro-tauri`). هیچ کنترلری نباید خودش این را چک کند.
 *
 * priority عمداً پایین‌تر از فایروال Symfony (که روی ۸ اجرا می‌شود) است تا توکن
 * احراز هویت ست شده باشد؛ در غیر این صورت `isGranted('ROLE_ADMIN')` همیشه false
 * برمی‌گشت و خودِ ادمین هم پشت صفحهٔ تعمیرات قفل می‌شد.
 */
class MaintenanceSubscriber implements EventSubscriberInterface
{
    /**
     * مسیرهایی که هرگز نباید مسدود شوند.
     *
     * بدون `/oauth` و مسیرهای ورود، ادمین نمی‌تواند لاگین کند و بدون
     * `/api/v1/admin/settings` راهی برای خاموش کردن حالت تعمیرات باقی نمی‌ماند.
     */
    private const WHITELIST_PREFIXES = [
        '/health',
        '/oauth/',
        '/session/token',
        '/api/v1/user/login',
        '/api/v1/user/send-code',
        '/api/v1/user/verify-code',
        '/api/v1/user/otp-login',
        '/api/v1/admin/settings',
        '/admin',
        '/build/',
        '/favicon.ico',
        '/_wdt',
        '/_profiler',
    ];

    public function __construct(
        private readonly MaintenanceService  $maintenance,
        private readonly Security            $security,
        private readonly Environment         $twig,
        private readonly JWTEncoderInterface $jwtEncoder,
    ) {}

    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::REQUEST   => ['onKernelRequest', 6],
            // بالاتر از ExceptionSubscriber (۱۰) تا خطاهای مسیریابی و احراز هویت —
            // که پیش از priority ۶ پرتاب می‌شوند — هم صفحهٔ تعمیرات بگیرند نه ۴۰۴/۴۰۱.
            KernelEvents::EXCEPTION => ['onKernelException', 20],
        ];
    }

    public function onKernelRequest(RequestEvent $event): void
    {
        if (!$event->isMainRequest()) {
            return;
        }

        $response = $this->maintenanceResponseFor($event->getRequest());
        if ($response === null) {
            return;
        }

        $event->setResponse($response);
        $event->stopPropagation();
    }

    public function onKernelException(ExceptionEvent $event): void
    {
        if (!$event->isMainRequest()) {
            return;
        }

        $response = $this->maintenanceResponseFor($event->getRequest());
        if ($response === null) {
            return;
        }

        $event->setResponse($response);
        $event->stopPropagation();
    }

    private function maintenanceResponseFor(Request $request): ?Response
    {
        $state = $this->maintenance->getState();
        if (!$state['enabled']) {
            return null;
        }
        if ($this->isWhitelisted($request->getPathInfo())) {
            return null;
        }
        if (in_array((string) $request->getClientIp(), $state['allowedIps'], true)) {
            return null;
        }
        if ($this->isAdmin($request)) {
            return null;
        }

        return $this->buildResponse($request, $state);
    }

    /**
     * چند مسیر پرمصرف پنل ادمین (`/api/v1/doctors`, `/api/v1/categorys/`, …) داخل
     * فایروال `public_endpoints` با `security: false` هستند؛ آن‌جا هیچ توکنی resolve
     * نمی‌شود و `isGranted` همیشه false است. برای همین اگر فایروال ادمین را نشناخت،
     * JWT هدر Authorization مستقیماً بررسی می‌شود.
     */
    private function isAdmin(Request $request): bool
    {
        if ($this->security->isGranted('ROLE_ADMIN')) {
            return true;
        }

        $header = (string) $request->headers->get('Authorization');
        if (!str_starts_with($header, 'Bearer ')) {
            return false;
        }

        try {
            $payload = $this->jwtEncoder->decode(substr($header, 7));
        } catch (\Throwable) {
            return false;
        }

        return in_array('ROLE_ADMIN', (array) ($payload['roles'] ?? []), true);
    }

    private function isWhitelisted(string $path): bool
    {
        foreach (self::WHITELIST_PREFIXES as $prefix) {
            if (str_starts_with($path, $prefix)) {
                return true;
            }
        }

        return false;
    }

    /** @param array{title:string,message:string,retryAfter:int} $state */
    private function buildResponse(Request $request, array $state): Response
    {
        $headers = ['Retry-After' => (string) $state['retryAfter']];

        if ($this->expectsJson($request)) {
            // شکل پاسخ باید دقیقاً با BaseController::error() یکی بماند تا کلاینت‌ها
            // بدون تغییر بتوانند آن را parse کنند.
            return new JsonResponse([
                'success' => false,
                'data'    => null,
                'errors'  => [[
                    'code'    => 'MAINTENANCE_MODE',
                    'message' => $state['message'],
                ]],
            ], Response::HTTP_SERVICE_UNAVAILABLE, $headers);
        }

        return new Response(
            $this->twig->render('maintenance.html.twig', [
                'title'   => $state['title'],
                'message' => $state['message'],
            ]),
            Response::HTTP_SERVICE_UNAVAILABLE,
            $headers,
        );
    }

    private function expectsJson(Request $request): bool
    {
        return str_starts_with($request->getPathInfo(), '/api/')
            || str_contains((string) $request->headers->get('Accept'), 'application/json')
            || $request->isXmlHttpRequest();
    }
}
