<?php

namespace App\Providers;

use App\Contracts\Images\SubjectDetectorInterface;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use App\Models\Artwork;
use App\Models\ArtworkAward;
use App\Models\ArtworkComment;
use App\Models\ArtworkFeature;
use App\Models\ArtworkFavourite;
use App\Models\ArtworkMedal;
use App\Models\ArtworkReaction;
use App\Models\ContentType;
use App\Models\GroupRelease;
use App\Models\GroupReleaseContributor;
use App\Models\HomepageAnnouncement;
use App\Observers\ArtworkAwardObserver;
use App\Observers\ArtworkCommentObserver;
use App\Observers\ArtworkFeatureObserver;
use App\Observers\ArtworkFavouriteObserver;
use App\Observers\ArtworkObserver;
use App\Observers\ArtworkReactionObserver;
use App\Observers\ContentTypeObserver;
use App\Observers\GroupReleaseContributorObserver;
use App\Observers\GroupReleaseObserver;
use App\Observers\HomepageAnnouncementObserver;
use App\Services\Upload\Contracts\UploadDraftServiceInterface;
use App\Services\Upload\UploadDraftService;
use App\Services\ContentTypes\ContentTypeSlugResolver;
use App\Services\Worlds\WorldService;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use Illuminate\Queue\Events\JobFailed;
use App\Services\ReceivedCommentsInboxService;
use App\Services\Images\Detectors\ChainedSubjectDetector;
use App\Services\Images\Detectors\HeuristicSubjectDetector;
use App\Services\Images\Detectors\NullSubjectDetector;
use App\Services\Images\Detectors\VisionSubjectDetector;
use Klevze\ControlPanel\Framework\Core\Menu;
use Laravel\Cashier\Events\WebhookHandled;
use Laravel\Cashier\Events\WebhookReceived;
use App\Listeners\Academy\HandleAcademyStripeWebhook;
use App\Listeners\Academy\HandleAcademyStripeWebhookHandled;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        $this->app->singleton(
            \App\Services\Countries\CountryRemoteProviderInterface::class,
            \App\Services\Countries\CountryRemoteProvider::class,
        );

        // Bind UploadDraftService interface to implementation
        $this->app->singleton(UploadDraftServiceInterface::class, function ($app) {
            return new UploadDraftService($app->make('filesystem'));
        });

        // Bind vector adapter interface for similarity system (resolves via factory)
        $this->app->bind(
            \App\Services\Recommendations\VectorSimilarity\VectorAdapterInterface::class,
            fn () => \App\Services\Recommendations\VectorSimilarity\VectorAdapterFactory::make(),
        );

        // EGS: bind SpotlightEngineInterface to the concrete SpotlightEngine
        $this->app->singleton(
            \App\Services\EarlyGrowth\SpotlightEngineInterface::class,
            \App\Services\EarlyGrowth\SpotlightEngine::class,
        );

        $this->app->singleton(SubjectDetectorInterface::class, function ($app) {
            return new ChainedSubjectDetector([
                $app->make(VisionSubjectDetector::class),
                $app->make(HeuristicSubjectDetector::class),
                $app->make(NullSubjectDetector::class),
            ]);
        });
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        $this->registerCpadMenuItems();

        // Map the 'legacy' view namespace to resources/views/_legacy so all
        // view('legacy::foo') and @include('legacy::foo') calls resolve correctly
        // after the folder was renamed from legacy/ to _legacy/.
        View::addNamespace('legacy', resource_path('views/_legacy'));

        $exceptionRendererComponentsPath = base_path('vendor/laravel/framework/src/Illuminate/Foundation/resources/exceptions/renderer/components');

        if (is_dir($exceptionRendererComponentsPath)) {
            Blade::anonymousComponentNamespace($exceptionRendererComponentsPath, 'laravel-exceptions-renderer');
        }

        $this->configureAuthRateLimiters();
        $this->configureUploadRateLimiters();
        $this->configureMessagingRateLimiters();
        $this->configureDownloadRateLimiter();
        $this->configureArtworkRateLimiters();
        $this->configureNovaCardRateLimiters();
        $this->configureReactionRateLimiters();
        $this->configureSocialRateLimiters();
        $this->configureSettingsRateLimiters();
        $this->configureMailFailureLogging();

        ArtworkAward::observe(ArtworkAwardObserver::class);
        ArtworkMedal::observe(ArtworkAwardObserver::class);
        Artwork::observe(ArtworkObserver::class);
        ArtworkFeature::observe(ArtworkFeatureObserver::class);
        ArtworkFavourite::observe(ArtworkFavouriteObserver::class);
        ArtworkComment::observe(ArtworkCommentObserver::class);
        ArtworkReaction::observe(ArtworkReactionObserver::class);
        ContentType::observe(ContentTypeObserver::class);
        GroupRelease::observe(GroupReleaseObserver::class);
        GroupReleaseContributor::observe(GroupReleaseContributorObserver::class);
        HomepageAnnouncement::observe(HomepageAnnouncementObserver::class);

        // ── OAuth / SocialiteProviders ──────────────────────────────────────
        Event::listen(
            \SocialiteProviders\Manager\SocialiteWasCalled::class,
            \SocialiteProviders\Discord\DiscordExtendSocialite::class,
        );
        // Apple provider removed — no listener registered

        // ── Posts / Feed System Events ──────────────────────────────────────
        Event::listen(
            \App\Events\Posts\ArtworkShared::class,
            \App\Listeners\Posts\SendArtworkSharedNotification::class,
        );
        Event::listen(
            \App\Events\Posts\PostCommented::class,
            \App\Listeners\Posts\SendPostCommentedNotification::class,
        );
        Event::listen(
            \App\Events\Posts\PostCommented::class,
            \App\Listeners\Posts\AwardXpForPostCommented::class,
        );
        Event::listen(
            \App\Events\Achievements\AchievementCheckRequested::class,
            \App\Listeners\Achievements\CheckUserAchievements::class,
        );
        Event::listen(
            \App\Events\Achievements\UserXpUpdated::class,
            \App\Listeners\Achievements\CheckUserAchievements::class,
        );
        Event::listen(
            WebhookReceived::class,
            HandleAcademyStripeWebhook::class,
        );
        Event::listen(
            WebhookHandled::class,
            HandleAcademyStripeWebhookHandled::class,
        );

        // Provide toolbar counts and user info to layout views (port of legacy toolbar logic)
        View::composer(['layouts.nova', 'layouts.nova.*'], function ($view) {
            $uploadCount = $favCount = $msgCount = $noticeCount = $receivedCommentsCount = 0;
            $avatarHash = null;
            $displayName = null;
            $userId = null;
            $toolbarContentTypes = collect();
            $toolbarActiveCampaign = null;
            $request = request();
            $canReadSessionAuth = $request instanceof \Illuminate\Http\Request
                && $request->hasSession()
                && $request->attributes->get('skinbase.session_skipped') !== true;
            $authUser = $canReadSessionAuth ? Auth::user() : null;

            try {
                $toolbarContentTypes = $this->app
                    ->make(ContentTypeSlugResolver::class)
                    ->toolbarContentTypes()
                    ->map(fn (ContentType $contentType) => $contentType->only(['id', 'name', 'slug']))
                    ->map(fn (array $attributes) => new ContentType($attributes));
            } catch (\Throwable $e) {
                $toolbarContentTypes = collect();
            }

            try {
                $toolbarActiveCampaign = $this->app
                    ->make(WorldService::class)
                    ->navigationCampaign();
            } catch (\Throwable $e) {
                $toolbarActiveCampaign = null;
            }

            if ($authUser) {
                $authUser->loadMissing('profile');
                $userId = (int) $authUser->id;

                try {
                    $ttl = (int) config('toolbar.cache_ttl_seconds', 30);

                    $stats = Cache::remember("toolbar:{$userId}", $ttl, function () use ($userId) {
                        $toolbarStats = DB::table('users')
                            ->select('id')
                            ->selectSub(
                                DB::table('artworks')
                                    ->selectRaw('COUNT(*)')
                                    ->whereColumn('user_id', 'users.id'),
                                'upload_count'
                            )
                            ->selectSub(
                                DB::table('artwork_favourites')
                                    ->selectRaw('COUNT(*)')
                                    ->whereColumn('user_id', 'users.id'),
                                'fav_count'
                            )
                            ->selectSub(
                                DB::table('notifications')
                                    ->selectRaw('COUNT(*)')
                                    ->whereColumn('user_id', 'users.id')
                                    ->whereNull('read_at'),
                                'notice_count'
                            )
                            ->where('id', $userId)
                            ->first();

                        $uploadCount = (int) ($toolbarStats->upload_count ?? 0);
                        $favCount = (int) ($toolbarStats->fav_count ?? 0);
                        $noticeCount = (int) ($toolbarStats->notice_count ?? 0);

                        $msgCount = (int) DB::table('conversation_participants as cp')
                            ->join('messages as m', 'm.conversation_id', '=', 'cp.conversation_id')
                            ->where('cp.user_id', $userId)
                            ->whereNull('cp.left_at')
                            ->whereNull('m.deleted_at')
                            ->where('m.sender_id', '!=', $userId)
                            ->where(function ($q) {
                                $q->whereNull('cp.last_read_at')
                                    ->orWhereColumn('m.created_at', '>', 'cp.last_read_at');
                            })
                            ->count();

                        return [
                            'upload_count' => $uploadCount,
                            'fav_count' => $favCount,
                            'notice_count' => $noticeCount,
                            'msg_count' => $msgCount,
                        ];
                    });

                    $uploadCount = (int) ($stats['upload_count'] ?? 0);
                    $favCount = (int) ($stats['fav_count'] ?? 0);
                    $noticeCount = (int) ($stats['notice_count'] ?? 0);
                    $msgCount = (int) ($stats['msg_count'] ?? 0);
                } catch (\Throwable $e) {
                    $uploadCount = 0;
                    $favCount = 0;
                    $msgCount = 0;
                    $noticeCount = 0;
                }

                try {
                    $receivedCommentsCount = $this->app->make(ReceivedCommentsInboxService::class)
                        ->unreadCountForUser($authUser);
                } catch (\Throwable $e) {
                    $receivedCommentsCount = 0;
                }

                $avatarHash = $authUser->profile?->avatar_hash;
                $displayName = $authUser->name ?: ($authUser->username ?? '');
            }

            $view->with(compact('userId','uploadCount', 'favCount', 'msgCount', 'noticeCount', 'receivedCommentsCount', 'avatarHash', 'displayName', 'toolbarContentTypes', 'toolbarActiveCampaign'));
        });

        // Replace the framework HandleCors with our ConditionalCors so the
        // CP_ENABLE_CORS / config('cors.paths') toggle takes effect.
        try {
            $middlewareConfig = $this->app->make(\Illuminate\Foundation\Configuration\Middleware::class);
            $middlewareConfig->replace(
                \Illuminate\Http\Middleware\HandleCors::class,
                \App\Http\Middleware\ConditionalCors::class
            );
        } catch (\Throwable $_) {
            // Fallback: push to kernel if replace isn't available in this app instance
            $this->app->make(\Illuminate\Contracts\Http\Kernel::class)
                ->pushMiddleware(\App\Http\Middleware\ConditionalCors::class);
        }
    }

    private function configureAuthRateLimiters(): void
    {
        RateLimiter::for('register-ip', function (Request $request): Limit {
            $limit = max(1, (int) config('registration.ip_per_minute_limit', 3));

            return Limit::perMinute($limit)->by('register:ip:' . $request->ip());
        });

        RateLimiter::for('register-ip-daily', function (Request $request): Limit {
            $limit = max(1, (int) config('registration.ip_per_day_limit', 20));

            return Limit::perDay($limit)->by('register:ip:daily:' . $request->ip());
        });

        RateLimiter::for('register', function (Request $request): array {
            $emailKey = strtolower((string) $request->input('email', 'unknown'));
            $ipLimit = (int) config('registration.ip_per_minute_limit', 3);
            $emailLimit = (int) config('registration.email_per_minute_limit', 6);

            return [
                Limit::perMinute($ipLimit)->by('register:ip:' . $request->ip()),
                Limit::perMinute($emailLimit)->by('register:email:' . $emailKey),
            ];
        });
    }

    private function configureMailFailureLogging(): void
    {
        Event::listen(JobFailed::class, function (JobFailed $event): void {
            if (! str_contains(strtolower($event->job->resolveName()), 'sendqueuedmailable')) {
                return;
            }

            Log::warning('mail delivery failed', [
                'transport' => config('mail.default'),
                'job_name' => $event->job->resolveName(),
                'queue' => $event->job->getQueue(),
                'connection' => $event->connectionName,
                'exception' => $event->exception->getMessage(),
            ]);
        });
    }

    private function configureUploadRateLimiters(): void
    {
        RateLimiter::for('uploads-init', function (Request $request): array {
            return $this->buildUploadLimits($request, 'init');
        });

        RateLimiter::for('uploads-chunk', function (Request $request): array {
            return $this->buildUploadLimits($request, 'chunk');
        });

        RateLimiter::for('uploads-finish', function (Request $request): array {
            return $this->buildUploadLimits($request, 'finish');
        });

        RateLimiter::for('uploads-status', function (Request $request): array {
            return $this->buildUploadLimits($request, 'status');
        });
    }

    private function configureNovaCardRateLimiters(): void
    {
        foreach (['drafts', 'autosave', 'publish', 'background_upload', 'render'] as $key) {
            RateLimiter::for('nova-cards-' . str_replace('_', '-', $key), function (Request $request) use ($key): array {
                $config = (array) config('nova_cards.rate_limits.' . $key, []);
                $perUser = max(1, (int) ($config['per_user'] ?? 30));
                $perIp = max(1, (int) ($config['per_ip'] ?? 60));
                $scope = 'nova-cards:' . $key . ':';

                return [
                    Limit::perMinute($perUser)->by($scope . 'user:' . ($request->user()?->id ?? 'guest')),
                    Limit::perMinute($perIp)->by($scope . 'ip:' . $request->ip()),
                ];
            });
        }
    }

    private function buildUploadLimits(Request $request, string $key): array
    {
        $config = (array) config('uploads.rate_limits.' . $key, []);
        $decay = (int) config('uploads.rate_limits.decay_minutes', 1);
        $perUser = (int) ($config['per_user'] ?? 0);
        $perIp = (int) ($config['per_ip'] ?? 0);

        $limits = [];

        if ($perUser > 0) {
            $userId = $request->user()?->id ?? 'guest';
            $limits[] = Limit::perMinutes($decay, $perUser)->by('u:' . $userId);
        }

        if ($perIp > 0) {
            $limits[] = Limit::perMinutes($decay, $perIp)->by('ip:' . $request->ip());
        }

        return $limits;
    }

    private function configureMessagingRateLimiters(): void
    {
        RateLimiter::for('messages-send', function (Request $request): array {
            $userId = $request->user()?->id ?? 'guest';

            return [
                Limit::perMinute(20)->by('messages:user:' . $userId),
                Limit::perMinute(40)->by('messages:ip:' . $request->ip()),
            ];
        });

        RateLimiter::for('messages-react', function (Request $request): array {
            $userId = $request->user()?->id ?? 'guest';

            return [
                Limit::perMinute(60)->by('messages:react:user:' . $userId),
                Limit::perMinute(120)->by('messages:react:ip:' . $request->ip()),
            ];
        });

        RateLimiter::for('messages-read', function (Request $request): array {
            $userId = $request->user()?->id ?? 'guest';

            return [
                Limit::perMinute(120)->by('messages:read:user:' . $userId),
                Limit::perMinute(240)->by('messages:read:ip:' . $request->ip()),
            ];
        });

        RateLimiter::for('messages-typing', function (Request $request): array {
            $userId = $request->user()?->id ?? 'guest';
            $conversationId = (int) $request->route('conversation_id');

            return [
                Limit::perMinute(90)->by('messages:typing:user:' . $userId . ':conv:' . $conversationId),
                Limit::perMinute(180)->by('messages:typing:ip:' . $request->ip()),
            ];
        });

        RateLimiter::for('messages-recovery', function (Request $request): array {
            $userId = $request->user()?->id ?? 'guest';
            $conversationId = (int) $request->route('conversation_id');

            return [
                Limit::perMinute(30)->by('messages:recovery:user:' . $userId . ':conv:' . $conversationId),
                Limit::perMinute(60)->by('messages:recovery:ip:' . $request->ip()),
            ];
        });

        RateLimiter::for('messages-presence', function (Request $request): array {
            $userId = $request->user()?->id ?? 'guest';

            return [
                Limit::perMinute(180)->by('messages:presence:user:' . $userId),
                Limit::perMinute(300)->by('messages:presence:ip:' . $request->ip()),
            ];
        });
    }

    private function configureDownloadRateLimiter(): void
    {
        RateLimiter::for('downloads', function (Request $request): array {
            $userId = $request->user()?->id;

            // Higher user-based allowance prevents false positives for active users,
            // while IP limit still protects guest endpoints from bursts.
            return [
                Limit::perMinute(60)->by('downloads:user:' . ($userId ?? 'guest')),
                Limit::perMinute(120)->by('downloads:ip:' . $request->ip()),
            ];
        });
    }

    private function configureArtworkRateLimiters(): void
    {
        RateLimiter::for('artwork-awards', function (Request $request): array {
            $userId = $request->user()?->id;
            $artworkId = (int) $request->route('id');
            $perMinute = max(1, (int) config('artwork_medals.rate_limit_per_minute', 10));

            return [
                // Prevent burst spam on a single artwork while allowing normal exploration.
                Limit::perMinute($perMinute)->by('awards:user:' . ($userId ?? 'guest') . ':art:' . $artworkId),
                // Global safety net for user/IP across all artworks.
                Limit::perMinute($perMinute * 6)->by('awards:user:' . ($userId ?? 'guest')),
                Limit::perMinute($perMinute * 9)->by('awards:ip:' . $request->ip()),
            ];
        });
    }

    private function configureReactionRateLimiters(): void
    {
        RateLimiter::for('reactions-read', function (Request $request): array {
            $userId = $request->user()?->id;

            return [
                // Comment-heavy pages can trigger many reaction reads at once.
                Limit::perMinute(600)->by('reactions-read:user:' . ($userId ?? 'guest')),
                Limit::perMinute(900)->by('reactions-read:ip:' . $request->ip()),
            ];
        });

        RateLimiter::for('reactions-write', function (Request $request): array {
            $userId = $request->user()?->id;

            return [
                Limit::perMinute(120)->by('reactions-write:user:' . ($userId ?? 'guest')),
                Limit::perMinute(180)->by('reactions-write:ip:' . $request->ip()),
            ];
        });
    }

    private function configureSocialRateLimiters(): void
    {
        RateLimiter::for('follow-write', function (Request $request): array {
            $userId = $request->user()?->id ?? 'guest';

            return [
                Limit::perMinute(12)->by('follow-write:minute:user:' . $userId),
                Limit::perHour(120)->by('follow-write:hour:user:' . $userId),
                Limit::perMinute(24)->by('follow-write:minute:ip:' . $request->ip()),
            ];
        });

        RateLimiter::for('social-write', function (Request $request): array {
            $userId = $request->user()?->id ?? 'guest';

            return [
                Limit::perMinute(60)->by('social-write:user:' . $userId),
                Limit::perMinute(120)->by('social-write:ip:' . $request->ip()),
            ];
        });

        RateLimiter::for('social-read', function (Request $request): array {
            $userId = $request->user()?->id ?? 'guest';

            return [
                Limit::perMinute(240)->by('social-read:user:' . $userId),
                Limit::perMinute(480)->by('social-read:ip:' . $request->ip()),
            ];
        });
    }

    private function configureSettingsRateLimiters(): void
    {
        RateLimiter::for('username-check', function (Request $request): Limit {
            $key = 'username-check:ip:' . $request->ip();

            if (method_exists(Limit::class, 'perSecond')) {
                return Limit::perSecond(5)->by($key);
            }

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

        RateLimiter::for('email-change-request', function (Request $request): Limit {
            $userId = $request->user()?->id;
            $key = $userId !== null
                ? 'email-change-request:user:' . $userId
                : 'email-change-request:ip:' . $request->ip();

            return Limit::perHour(1)->by($key);
        });
    }

    private function registerCpadMenuItems(): void
    {
        if (! $this->isControlPanelRequest()) {
            return;
        }

        if (! class_exists(Menu::class)) {
            return;
        }

        try {
            /** @var Menu $menu */
            $menu = $this->app->make(Menu::class);
        } catch (\Throwable) {
            // Control panel menu registration should never block the app boot.
        }
    }

    private function isControlPanelRequest(): bool
    {
        if ($this->app->runningInConsole()) {
            return true;
        }

        if (! $this->app->bound('request')) {
            return false;
        }

        $prefix = trim((string) config('cpad.webroot', config('cp.webroot', '/cp')), '/');

        if ($prefix === '') {
            return false;
        }

        $request = $this->app->make('request');

        return $request->is($prefix) || $request->is($prefix . '/*');
    }
}
