<?php

namespace App\Providers;

use App\Enums\Role;
use App\Http\Middleware\EnsureSecurityOnboarded;
use App\Models\Setting;
use App\Models\User;
use App\Services\DeploymentService;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Vite;
use Illuminate\Support\ServiceProvider;
use Livewire\Livewire;
use Throwable;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        //
    }

    /**
     * Apply the effective panel domain (dashboard override in DB, else install-time
     * APP_DOMAIN) to runtime config at boot. boot() runs every request, so a domain
     * change takes effect on the next request after the operator restarts the stack —
     * nothing is switched mid-session, which is what makes it safe.
     *
     * - Server -> Reverb publishing is pinned to the INTERNAL reverb service (both dev
     *   and prod compose name it `reverb`), so realtime keeps working right after a
     *   domain change and never depends on the new public cert. The browser endpoint is
     *   injected by the layout from DeploymentService::reverbClient().
     * - app.url -> https://<domain> so absolute URL generation matches how the panel is
     *   served. With no domain (bare IP / dev) the env-derived app.url is left as is.
     */
    public function boot(DeploymentService $deployment): void
    {
        // Drop Vite's <link rel="preload"> hints: with only one CSS + one JS entry the preload
        // sits right next to the matching stylesheet/script tags, so it adds no real value — but
        // it triggers Chrome's "preloaded but not used" console warning (especially with DevTools
        // "Disable cache", which double-fetches the CSS so the preloaded copy is never consumed).
        Vite::usePreloadTagAttributes(false);

        // Re-apply the onboarding gate to EVERY Livewire component update, not just the
        // initial page GET. Livewire only re-runs route middleware that is registered as
        // persistent; without this, a component mounted while onboarded could keep running
        // destructive actions (hardening, SSH-key provisioning, credential changes) over
        // /livewire/update after the account is reverted to must_change_password mid-session.
        Livewire::addPersistentMiddleware([
            EnsureSecurityOnboarded::class,
        ]);

        // RBAC abilities. admin has every dangerous control-plane ability; operator can run
        // day-to-day operations; viewer is read-only. Gates are the single source consumed by route
        // `can:` middleware, component mount() abort_unless, per-action guards and @can in Blade.
        foreach (['manage-panel', 'manage-users', 'manage-network', 'manage-fleet'] as $ability) {
            Gate::define($ability, fn (User $u) => $u->isAdmin());
        }
        Gate::define('operate', fn (User $u) => $u->roleAtLeast(Role::Operator));

        // Defense-in-depth: a coarse per-identity cap on the Livewire action endpoint
        // (/livewire/update carries every component request, including the auth actions). The
        // per-action limiters in the components are the precise gate; this is the blunt cap so a
        // single source (authed user id, else guest IP) can't flood workers regardless of which
        // action it targets. 180/min sits far above normal polling + interaction yet well below a
        // flood; it auto-expires, so — like every limiter here — it can never permanently lock the
        // control plane (the bare-IP recovery host and clusev:reset-admin are unaffected).
        RateLimiter::for('livewire-update', fn (Request $request) => Limit::perMinute(180)
            ->by($request->user()?->id ?: $request->ip()));

        Livewire::setUpdateRoute(fn ($handle) => Route::post('/livewire/update', $handle)
            ->middleware(['web', 'throttle:livewire-update']));

        config([
            'broadcasting.connections.reverb.options.host' => 'reverb',
            'broadcasting.connections.reverb.options.port' => 8080,
            'broadcasting.connections.reverb.options.scheme' => 'http',
            'broadcasting.connections.reverb.options.useTLS' => false,
        ]);

        $domain = $deployment->domain();
        if ($domain !== null) {
            config(['app.url' => 'https://'.$domain]);
        }

        $this->applyMailSettings();
    }

    /**
     * Apply the operator's SMTP settings (Settings\Email) to runtime config so
     * password-reset mails and notifications actually send. boot() runs every request,
     * so a saved change takes effect on the next request.
     *
     * Only switches to the `smtp` mailer when host + from-address are configured;
     * otherwise the safe install default (`log`) is left untouched, so nothing breaks
     * while unconfigured. The stored SMTP password is encrypted at rest (APP_KEY) and
     * decrypted here. Wrapped in try/catch like DeploymentService — the settings table
     * may not exist yet during install/migrate, and that must never break boot.
     */
    private function applyMailSettings(): void
    {
        try {
            $host = Setting::get('mail_host');
            $from = Setting::get('mail_from_address');

            if ($host === null || $host === '' || $from === null || $from === '') {
                return; // unconfigured -> keep the safe default mailer
            }

            $encryption = Setting::get('mail_encryption', 'tls');
            $storedPassword = Setting::get('mail_password');
            $password = null;
            if ($storedPassword !== null && $storedPassword !== '') {
                $password = Crypt::decryptString($storedPassword);
            }

            config([
                'mail.default' => 'smtp',
                'mail.mailers.smtp.host' => $host,
                'mail.mailers.smtp.port' => (int) (Setting::get('mail_port', '587') ?? 587),
                'mail.mailers.smtp.username' => Setting::get('mail_username') ?: null,
                'mail.mailers.smtp.password' => $password ?: null,
                'mail.mailers.smtp.encryption' => $encryption === 'none' ? null : $encryption,
                'mail.from.address' => $from,
                'mail.from.name' => Setting::get('mail_from_name') ?: $from,
            ]);
        } catch (Throwable) {
            // Settings table not migrated yet (install/migrate) or an undecryptable value —
            // leave the default mailer untouched. Never break boot.
        }
    }
}
