<?php

namespace App\Providers;

use App\Http\Middleware\IdentifyTenant;
use App\Models\Role;
use App\Models\User;
use App\Notifications\WhatsApp\NullWhatsAppGateway;
use App\Notifications\WhatsApp\TwilioWhatsAppGateway;
use App\Notifications\WhatsApp\WhatsAppGateway;
use App\Policies\RolePolicy;
use App\Policies\UserPolicy;
use App\Services\CurrentContext;
use App\Services\Storage\StorageConfig;
use App\Services\Storage\TenantStorage;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ForceDeleteAction;
use Filament\Actions\RestoreAction;
use Filament\Actions\ViewAction;
use Filament\Resources\Pages\EditRecord;
use Filament\Resources\Pages\ListRecords;
use Filament\Resources\Pages\ManageRecords;
use Filament\Resources\Pages\ViewRecord;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Queue\Events\JobProcessing;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;
use Livewire\Livewire;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(WhatsAppGateway::class, function () {
            return match (config('services.whatsapp.driver', 'null')) {
                'twilio' => new TwilioWhatsAppGateway,
                default => new NullWhatsAppGateway,
            };
        });

        $this->app->singleton(CurrentContext::class);
        $this->app->singleton(TenantStorage::class);
        // Singleton so the instance TenantStorage was constructed with IS the
        // one every other caller (and StorageSetting::writeConfig's flush)
        // reaches — otherwise its per-request memo could never be invalidated.
        $this->app->singleton(StorageConfig::class);
    }

    public function boot(): void
    {
        // Path tenancy (docs/path-based-tenancy-plan.md Lever 3): Livewire's
        // AJAX endpoints (update/upload/preview) live at the site root and
        // carry no {workspace} segment, so IdentifyTenant cannot resolve the
        // tenant from the real request's URL. Registered as PERSISTENT
        // middleware, it is re-run by Livewire against a fake request rebuilt
        // at the original page path (from the component snapshot's `path`
        // memo), where the {workspace} route parameter is resolvable again.
        // Filament registers its own IdentifyTenant the same way
        // (vendor/filament/filament/src/FilamentServiceProvider.php), which is
        // the sanctioned mechanism. The existing priority pinning in
        // bootstrap/app.php applies here too: persistent middleware is
        // gathered via Router::gatherRouteMiddleware(), which sorts by the
        // same middleware priority list.
        Livewire::addPersistentMiddleware([IdentifyTenant::class]);

        Gate::policy(User::class, UserPolicy::class);
        Gate::policy(Role::class, RolePolicy::class);

        $this->keepStorageConfigFreshInQueueWorkers();

        $this->enforceResourceAuthorizationOnBuiltInActions();

        // Ported verbatim from the source app, with ONE necessary widening: the
        // parameter is `Authenticatable`, not `User`, and anything that is not a
        // tenant User abstains (returns null).
        //
        // The source type-hints `User` here. That never breaks it, because its
        // central/landlord panel is Inertia and performs its checks by hand — the
        // Gate is never consulted while a CentralAdmin is authenticated. Filament
        // consults the Gate on every resource and page, so on the central panel
        // the original signature raises a TypeError before any authorization
        // decision is reached (docs/migration-plan.md Decisions Log #64).
        //
        // For tenant users the behaviour is byte-identical: inactive → deny,
        // full admin → allow, otherwise defer to the policy.
        Gate::before(function (Authenticatable $user, string $ability): ?bool {
            if (! $user instanceof User) {
                return null;
            }

            if (! $user->isActive()) {
                return false;
            }

            if ($user->isFullAdmin()) {
                return true;
            }

            return null;
        });
    }

    /**
     * Route Filament's built-in Create/View/Edit/Delete actions through the
     * owning Resource's own `canX()` methods.
     *
     * Filament authorizes these actions against a Laravel **model policy**
     * (`get_authorization_response('delete', $record)`), and when a model has no
     * policy it treats that as "unrestricted" and allows. This app does not use
     * policies for most models — {@see \App\Filament\Resources\Concerns\HasModulePermissions}
     * overrides `canCreate()`/`canView()`/`canEdit()`/`canDelete()` on the
     * Resource instead — and Filament never consults those for table actions.
     *
     * The result was that only *page-level* access was really enforced. Every
     * row's Edit and Delete button (19 and 20 Resources respectively) rendered
     * for anyone who could open the listing, and Delete did not merely render:
     * a user holding `isms_products.access` alone could delete products,
     * because nothing between the button and `$record->delete()` ever asked.
     * `CreateAction` had the same gap on all 26 listings, though there the
     * Create page's own `authorizeAccess()` caught the click with a 403 — a
     * visible button that always failed.
     *
     * Applied centrally via `configureUsing()` rather than by chaining
     * `->authorize()` onto ~65 call sites, because the failure mode is silent:
     * a new listing that forgets the check looks completely normal to anyone
     * testing as an administrator. `configureUsing()` runs inside `make()`, so a
     * Resource can still override any of this with its own
     * `->authorize()`/`->visible()`/`->hidden()` afterwards.
     *
     * Unauthorized actions are hidden, never disabled — see
     * tests/Feature/Permissions/UiVisibilityTest.php.
     */
    private function enforceResourceAuthorizationOnBuiltInActions(): void
    {
        /**
         * The Resource behind the component an action is rendered in, or null
         * when it is not a resource page. Relation managers are deliberately
         * excluded: they carry their own rules, and the central panel's set
         * `$shouldSkipAuthorization` because it is gated at the panel level.
         */
        $resourceFor = static function (mixed $livewire): ?string {
            if (! $livewire instanceof ListRecords
                && ! $livewire instanceof ManageRecords
                && ! $livewire instanceof ViewRecord
                && ! $livewire instanceof EditRecord) {
                return null;
            }

            return $livewire::getResource();
        };

        CreateAction::configureUsing(function (CreateAction $action) use ($resourceFor): void {
            $action->authorize(static fn (mixed $livewire): bool => ($resource = $resourceFor($livewire)) === null
                || $resource::canCreate());
        });

        ViewAction::configureUsing(function (ViewAction $action) use ($resourceFor): void {
            $action->authorize(static fn (mixed $livewire, ?Model $record): bool => $record === null
                || ($resource = $resourceFor($livewire)) === null
                || $resource::canView($record));
        });

        EditAction::configureUsing(function (EditAction $action) use ($resourceFor): void {
            $action->authorize(static fn (mixed $livewire, ?Model $record): bool => $record === null
                || ($resource = $resourceFor($livewire)) === null
                || $resource::canEdit($record));
        });

        DeleteAction::configureUsing(function (DeleteAction $action) use ($resourceFor): void {
            $action->authorize(static fn (mixed $livewire, ?Model $record): bool => $record === null
                || ($resource = $resourceFor($livewire)) === null
                || $resource::canDelete($record));
        });

        // Not currently used by any Resource, configured so that the first one to
        // reach for them inherits the gate rather than the policy default.
        ForceDeleteAction::configureUsing(function (ForceDeleteAction $action) use ($resourceFor): void {
            $action->authorize(static fn (mixed $livewire, ?Model $record): bool => $record === null
                || ($resource = $resourceFor($livewire)) === null
                || $resource::canForceDelete($record));
        });

        RestoreAction::configureUsing(function (RestoreAction $action) use ($resourceFor): void {
            $action->authorize(static fn (mixed $livewire, ?Model $record): bool => $record === null
                || ($resource = $resourceFor($livewire)) === null
                || $resource::canRestore($record));
        });

        DeleteBulkAction::configureUsing(function (DeleteBulkAction $action) use ($resourceFor): void {
            $action->authorize(static fn (mixed $livewire): bool => ($resource = $resourceFor($livewire)) === null
                || $resource::canDeleteAny());
        });
    }

    /**
     * A queue worker's container outlives any number of credential edits made
     * from the web, and StorageConfig memoises its resolution for speed (one
     * `storage_settings` query + one AES decrypt would otherwise be paid per
     * product image rendered). Dropping the memo at the head of every job
     * restores the freshness guarantee the memo trades away, without paying
     * for it on the read path.
     */
    private function keepStorageConfigFreshInQueueWorkers(): void
    {
        Event::listen(JobProcessing::class, function (): void {
            app(StorageConfig::class)->flush();
        });
    }
}
