<?php

namespace App\Providers;

use App\Models\Creative;
use App\Observers\CreativeObserver;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
use Laravel\Ai\Models\Conversation;
use Livewire\Mechanisms\FrontendAssets\FrontendAssets;
use Livewire\Mechanisms\HandleRequests\HandleRequests;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        // Livewire's own getUpdateUri() always requests a domain-root-relative
        // path (Laravel's relative-URL generator strips any subdirectory the app
        // is served from), which breaks when the app is deployed under a URL
        // subpath (e.g. local dev at /php85/{user}/arb.admedia.com/public).
        // Forcing it absolute keeps the correct path in every environment,
        // subdirectory or domain root, with no per-environment config needed.
        //
        // NOTE: This override was originally added for subdirectory deployment.
        // After migrating to `artisan serve` at the domain root, verify if this
        // is still needed. If not, this override can be removed entirely.
        $this->app->extend(HandleRequests::class, function (HandleRequests $originalHandler) {
            // Create a new instance that extends HandleRequests, preserving
            // any properties that were set on the original handler by Livewire.
            $subclass = new class($originalHandler) extends HandleRequests
            {
                private HandleRequests $original;

                public function __construct(HandleRequests $original)
                {
                    $this->original = $original;
                    // Copy over any properties from the original instance
                    // (though in practice, Livewire hasn't set many at this point)
                    foreach (get_object_vars($original) as $key => $value) {
                        $this->{$key} = $value;
                    }
                }

                public function getUpdateUri()
                {
                    $route = $this->updateRoute ?? $this->findUpdateRoute();

                    return (string) app('url')->toRoute($route, [], true);
                }
            };

            return $subclass;
        });
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        Paginator::useBootstrapFive();

        RateLimiter::for('public-leads', function (Request $request) {
            return Limit::perMinute(10)->by((string) $request->ip());
        });

        RateLimiter::for('public-catalog-clickouts', function (Request $request) {
            return Limit::perMinute(120)->by((string) $request->ip());
        });

        RateLimiter::for('public-prompt-downloads', function (Request $request) {
            return Limit::perMinute(120)->by((string) $request->ip());
        });

        RateLimiter::for('prompt-landing-page-editor', function (Request $request) {
            return Limit::perMinute(30)->by((string) ($request->user()?->id ?: $request->ip()));
        });

        // Absolute URLs keep the scheme the site is actually served over.
        //
        // Only where there is a request to be served. This read
        // request()->getHost() unconditionally, which builds a Request out of
        // $_SERVER inside a provider's boot: an artisan command or a queue
        // worker has no request, so what came back was a placeholder whose
        // host happened to be "localhost", and the rule was being decided from
        // a value that meant nothing. Nothing broke, because a console process
        // generates URLs from APP_URL anyway, but the condition was answering a
        // question it could not ask.
        //
        // Worth revisiting rather than keeping forever: bootstrap/app.php
        // already trusts every proxy, so Laravel recovers the real scheme from
        // X-Forwarded-Proto on its own and this is only load bearing where the
        // load balancer does not send that header. Somebody who can check the
        // balancer should confirm, and delete this if it does.
        if (! $this->app->runningInConsole()
            && str_starts_with((string) config('app.url'), 'https://')
            // The dev server is reached over plain http at an IP and a path
            // while APP_URL names the https host, so forcing there would
            // produce links to a host that serves no TLS.
            && ! in_array(request()->getHost(), ['localhost', '127.0.0.1', '192.168.30.106'], true)
            && ! str_contains((string) config('app.url'), '192.168.30.106/php85/')
        ) {
            URL::forceScheme('https');
        }

        // A finished creative becomes a publishable asset. Registered as an
        // observer rather than called at each generator, because there are
        // three completion points and a fourth would otherwise be missed.
        Creative::observe(CreativeObserver::class);

        // Same subdirectory issue for the Livewire JS asset's <script src>:
        // it defaults to a bare "/livewire/livewire.js" (or .min.js in
        // production) unless this is set. Building it from the actual
        // registered route's URI + url() keeps it correct in any environment
        // (subdirectory in dev, plain domain in staging/prod) without
        // hardcoding the debug/production filename ourselves.
        if ($jsRoute = app(FrontendAssets::class)->javaScriptRoute) {
            config([
                'livewire.asset_url' => rtrim((string) config('app.url'), '/').'/'.ltrim($jsRoute->uri(), '/'),
            ]);
        }

        // The AdStudio nav panel (recent creatives) is shared shell chrome, rendered on
        // every page via partials.mb.sidebar — a composer avoids threading this through
        // every page controller/Livewire component that renders the mb layout.
        View::composer('partials.mb.sidebar', function ($view) {
            // The sidebar renders on every page in this layout, so this list is
            // the one place a chat can be seen without going through the chat
            // controller. It has to be scoped the same way that controller
            // scopes it, or the list quietly becomes everybody's.
            //
            // Only filled when the page did not supply its own: ChatController
            // and GoogleChatController both pass a scoped list already, and
            // this covers the pages that render the shell without one, such as
            // the dashboard, creatives and landing pages.
            $data = $view->getData();

            if (! isset($data['conversations'])) {
                $view->with('conversations', $this->sidebarConversations());
            }

            $view->with('recentCreatives', auth()->check()
                ? auth()->user()->creatives()->latest('id')->limit(5)->get()
                : collect());

            $view->with('recentPromptPages', auth()->check()
                ? auth()->user()->promptLandingPages()->latest('id')->limit(5)->get()
                : collect());
        });
    }

    /**
     * The recent chats shown in the shell's sidebar.
     *
     * Scoped to whoever is signed in. This list renders on every page in the
     * layout, so it is the one place a chat can be seen without going through
     * a chat controller, and it was not scoped at all: any signed-in user saw
     * the five most recent conversations in the system. The creatives and
     * landing pages beside it in the same composer were scoped, which is what
     * makes it look like an oversight rather than a decision.
     *
     * Only consulted when the page did not supply its own list. ChatController
     * and GoogleChatController both pass a scoped one; this covers the pages
     * that render the shell without one, such as the dashboard and creatives.
     *
     * @return Collection<int, Conversation>
     */
    private function sidebarConversations(): Collection
    {
        $user = auth()->user();

        // hasTable rather than a try/catch around the whole thing. The catch
        // it replaces swallowed every Throwable and reported a broken query as
        // "no chats", which is indistinguishable from the truth and hides the
        // failure it is standing in front of.
        if ($user === null || ! Schema::hasTable('agent_conversations')) {
            return collect();
        }

        $isGoogleChat = request()->routeIs('googlechat.*');
        $hasChatType = Schema::hasColumn('agent_conversations', 'chat_type');

        return Conversation::query()
            ->where('participant_type', $user->getMorphClass())
            ->where('participant_id', $user->getAuthIdentifier())
            // 'google_ads' is the value the column actually holds. This asked
            // for 'google', which nothing anywhere writes, so the branch could
            // never match; it was harmless only because the Google chat passes
            // its own list and never reaches this.
            ->when($hasChatType && $isGoogleChat, fn ($q) => $q->where('chat_type', 'google_ads'))
            ->when($hasChatType && ! $isGoogleChat, fn ($q) => $q->whereNull('chat_type'))
            ->latest('updated_at')
            ->limit(5)
            ->get();
    }
}
