<?php

namespace App\Shared\EventSubscriber;

use App\Shared\Constant\ErrorCodes;
use App\Shared\Exception\AppException;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;

class ExceptionSubscriber implements EventSubscriberInterface
{
    public function __construct(
        private readonly LoggerInterface    $logger,
        private readonly TokenStorageInterface $tokenStorage,
    ) {}

    public function onKernelException(ExceptionEvent $event): void
    {
        if (str_starts_with($event->getRequest()->getPathInfo(), '/api/doc')) {
            return;
        }

        $exception = $event->getThrowable();

        if ($exception instanceof AppException) {
            $err = ['code' => $exception->getErrorCode(), 'message' => $exception->getMessage()];
            if ($exception->getField()) {
                $err['field'] = $exception->getField();
            }
            $event->setResponse(new JsonResponse(
                ['success' => false, 'data' => null, 'errors' => [$err]],
                $exception->getHttpStatus()
            ));
            return;
        }

        if ($exception instanceof TooManyRequestsHttpException) {
            $headers = [];
            $retryAfter = $exception->getHeaders()['Retry-After'] ?? null;
            if ($retryAfter !== null) {
                $headers['Retry-After'] = $retryAfter;
            }
            $event->setResponse(new JsonResponse(
                ['success' => false, 'data' => null, 'errors' => [['code' => 'ERR_RATE_LIMIT_001', 'message' => 'درخواست‌های زیاد. لطفاً بعداً تلاش کنید']]],
                429,
                $headers
            ));
            return;
        }

        if ($exception instanceof NotFoundHttpException) {
            $event->setResponse(new JsonResponse(
                ['success' => false, 'data' => null, 'errors' => [['code' => 'ERR_NOT_FOUND_001', 'message' => 'منبع درخواستی یافت نشد']]],
                404
            ));
            return;
        }

        if ($exception instanceof AccessDeniedHttpException) {
            $event->setResponse(new JsonResponse(
                ['success' => false, 'data' => null, 'errors' => [['code' => 'ERR_FORBIDDEN_001', 'message' => 'دسترسی به این منبع مجاز نیست']]],
                403
            ));
            return;
        }

        if ($exception instanceof UnauthorizedHttpException) {
            $event->setResponse(new JsonResponse(
                ['success' => false, 'data' => null, 'errors' => [['code' => 'ERR_AUTH_001', 'message' => 'احراز هویت لازم است']]],
                401
            ));
            return;
        }

        // Security exceptions not yet wrapped into HttpException
        if ($exception instanceof AccessDeniedException) {
            $token = $this->tokenStorage->getToken();
            $isAuthenticated = $token !== null && $token->getUser() !== null;

            if (!$isAuthenticated) {
                $event->setResponse(new JsonResponse(
                    ['success' => false, 'data' => null, 'errors' => [['code' => 'ERR_AUTH_001', 'message' => 'احراز هویت لازم است']]],
                    401
                ));
            } else {
                $event->setResponse(new JsonResponse(
                    ['success' => false, 'data' => null, 'errors' => [['code' => 'ERR_FORBIDDEN_001', 'message' => 'دسترسی به این منبع مجاز نیست']]],
                    403
                ));
            }
            return;
        }

        if ($exception instanceof AuthenticationException) {
            $event->setResponse(new JsonResponse(
                ['success' => false, 'data' => null, 'errors' => [['code' => 'ERR_AUTH_001', 'message' => 'احراز هویت لازم است']]],
                401
            ));
            return;
        }

        // Bots/scanners and stray preflight hit disallowed methods on public paths (e.g. POST /).
        // Client errors, not server faults: respond 405 and log at notice so they don't flood
        // the error channel through the generic fallback below.
        if ($exception instanceof MethodNotAllowedHttpException) {
            $this->logger->notice('Method not allowed', [
                'path'   => $event->getRequest()->getPathInfo(),
                'method' => $event->getRequest()->getMethod(),
            ]);
            $event->setResponse(new JsonResponse(
                ['success' => false, 'data' => null, 'errors' => [['code' => ErrorCodes::ERR_METHOD_NOT_ALLOWED_001, 'message' => ErrorCodes::message(ErrorCodes::ERR_METHOD_NOT_ALLOWED_001)]]],
                405
            ));
            return;
        }

        // Generic fallback: never leak stack traces or internal details in API responses.
        // The class/message/location go into the log MESSAGE itself so they surface in
        // platforms (e.g. Liara) whose default logger only prints the message string.
        $this->logger->error(sprintf(
            'Unhandled exception: %s: %s @ %s:%d [path=%s]',
            $exception::class,
            $exception->getMessage(),
            $exception->getFile(),
            $exception->getLine(),
            $event->getRequest()->getPathInfo(),
        ), [
            'exception' => $exception,
        ]);

        $event->setResponse(new JsonResponse(
            ['success' => false, 'data' => null, 'errors' => [['code' => 'ERR_INTERNAL_001', 'message' => 'خطای داخلی سرور']]],
            500
        ));
    }

    public static function getSubscribedEvents(): array
    {
        return [KernelEvents::EXCEPTION => ['onKernelException', 10]];
    }
}
