<?php

namespace App\Exceptions;

use App\Support\ApiError;
use App\Support\SentryReporter;
use Illuminate\Database\Connectors\ConnectionException as DatabaseConnectionException;
use Illuminate\Database\QueryException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\Client\ConnectionException as HttpClientConnectionException;
use Illuminate\Queue\InvalidQueueException;
use Illuminate\Queue\MaxAttemptsExceededException;
use Illuminate\Routing\Exceptions\InvalidSignatureException;
use Illuminate\Session\TokenMismatchException;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
use League\Flysystem\FilesystemException;
use PDOException;
use Sentry\State\Scope;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Throwable;

class Handler extends ExceptionHandler
{
    /**
     * The list of the inputs that are never flashed to the session on validation exceptions.
     *
     * @var array<int, string>
     */
    protected $dontFlash = [
        'current_password',
        'password',
        'password_confirmation',
    ];

    /**
     * Register the exception handling callbacks for the application.
     */
    public function register(): void
    {
        $this->reportable(function (Throwable $e) {
            if ($this->shouldSkipSentry($e)) {
                return;
            }

            $this->configureSentryScope();

            SentryReporter::captureException($e);
        });
    }

    public function render($request, Throwable $e)
    {
        if ($e instanceof TokenMismatchException) {
            $this->logCsrfMismatch($request, 'token_mismatch');
        } elseif ($e instanceof HttpExceptionInterface && $e->getStatusCode() === 419) {
            $this->logCsrfMismatch($request, 'http_419');
        }

        if ($request->expectsJson()) {
            if ($e instanceof ValidationException) {
                return ApiError::response(
                    'validation_failed',
                    'Validation failed',
                    'The given data was invalid.',
                    422,
                    ['errors' => $e->errors()],
                );
            }

            $status = $this->isHttpException($e)
                ? $this->toHttpException($e)->getStatusCode()
                : 500;

            $code = $status >= 500 ? 'server_error' : 'request_failed';

            return ApiError::response(
                $code,
                $status >= 500 ? 'Unexpected error' : 'Request could not be completed',
                $this->buildGenericMessage($status),
                $status,
            );
        }

        if ($request->inertia()) {
            if ($e instanceof ValidationException) {
                return back()->withErrors($e->errors())->withInput($request->all());
            }
        }

        if ($e instanceof InvalidSignatureException && ! $request->expectsJson()) {
            $request->session()->flash('verification', [
                'status' => 'error',
                'title' => __('auth.verification.expired_title'),
                'message' => __('auth.verification.expired_message'),
            ]);

            return redirect()->route('verification.notice');
        }

        if (! $request->expectsJson() && ! $request->inertia()) {
            if ($hintKey = $this->resolveServerErrorHint($e)) {
                $request->attributes->set('serverErrorHint', __($hintKey, [], app()->getLocale()));
            }
        }

        return parent::render($request, $e);
    }

    private function configureSentryScope(): void
    {
        if (! function_exists('\Sentry\configureScope')) {
            return;
        }

        \Sentry\configureScope(function (Scope $scope): void {
            $user = Auth::user();

            if ($user) {
                $userData = [
                    'id' => (string) $user->getAuthIdentifier(),
                ];

                if (! empty($user->email)) {
                    $userData['email'] = $user->email;
                }

                if (! empty($user->username)) {
                    $userData['username'] = $user->username;
                }

                $scope->setUser($userData);

                if (! empty($user->tenant_id)) {
                    $scope->setTag('tenant_id', (string) $user->tenant_id);
                }

                if (! empty($user->role)) {
                    $scope->setTag('user_role', $user->role);
                }
            }
        });
    }

    private function shouldSkipSentry(Throwable $throwable): bool
    {
        if ($throwable instanceof ValidationException) {
            return true;
        }

        $status = null;

        if ($this->isHttpException($throwable)) {
            $status = $this->toHttpException($throwable)->getStatusCode();
        } elseif ($throwable instanceof HttpExceptionInterface) {
            $status = $throwable->getStatusCode();
        }

        if (is_int($status) && $status < 500 && $status !== 429) {
            return true;
        }

        return false;
    }

    private function buildGenericMessage(int $status): string
    {
        if ($status >= 500) {
            return 'Something went wrong on our side. Please try again later.';
        }

        return 'Your request could not be processed. Please verify the details and try again.';
    }

    private function resolveServerErrorHint(Throwable $throwable): ?string
    {
        $status = $this->isHttpException($throwable)
            ? $this->toHttpException($throwable)->getStatusCode()
            : 500;

        if ($status < 500) {
            return null;
        }

        if ($this->exceptionChainContains($throwable, [
            QueryException::class,
            DatabaseConnectionException::class,
            PDOException::class,
            'Doctrine\\DBAL\\Exception',
        ])) {
            return 'marketing.server_error.hints.database';
        }

        if ($this->exceptionChainContains($throwable, [
            FilesystemException::class,
            'League\\Flysystem\\UnableToWriteFile',
            'League\\Flysystem\\UnableToCreateDirectory',
            'League\\Flysystem\\UnableToCheckExistence',
        ])) {
            return 'marketing.server_error.hints.storage';
        }

        if ($this->exceptionChainContains($throwable, [
            InvalidQueueException::class,
            MaxAttemptsExceededException::class,
            'RedisException',
            'Predis\\Connection\\ConnectionException',
        ])) {
            return 'marketing.server_error.hints.queue';
        }

        if ($this->exceptionChainContains($throwable, [
            HttpClientConnectionException::class,
            'GuzzleHttp\\Exception\\ConnectException',
            'Psr\\Http\\Client\\NetworkExceptionInterface',
            'Symfony\\Component\\HttpClient\\Exception\\TransportExceptionInterface',
        ])) {
            return 'marketing.server_error.hints.network';
        }

        return 'marketing.server_error.hints.generic';
    }

    private function exceptionChainContains(Throwable $throwable, array $classNames): bool
    {
        do {
            foreach ($classNames as $className) {
                if ($this->throwableIsInstanceOf($throwable, $className)) {
                    return true;
                }
            }

            $throwable = $throwable->getPrevious();
        } while ($throwable);

        return false;
    }

    private function throwableIsInstanceOf(Throwable $throwable, string $className): bool
    {
        if ($className === '') {
            return false;
        }

        if (! class_exists($className) && ! interface_exists($className)) {
            return false;
        }

        return $throwable instanceof $className;
    }

    private function logCsrfMismatch($request, string $reason): void
    {
        if (! app()->environment('development')) {
            return;
        }

        Log::warning('[CSRF] Token mismatch', [
            'reason' => $reason,
            'method' => $request->method(),
            'path' => $request->path(),
            'full_url' => $request->fullUrl(),
            'ip' => $request->ip(),
            'user_id' => optional($request->user())->getAuthIdentifier(),
        ]);
    }
}
