<?php

namespace App\Livewire\Admin;

use App\Livewire\Concerns\ResolvesOperator;
use App\Models\VpnPeer;
use App\Services\Wireguard\ConfigHandoff;
use App\Services\Wireguard\ConfigVault;
use App\Services\Wireguard\QrCode;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use LivewireUI\Modal\ModalComponent;

/**
 * Retrieving a stored VPN config again, behind the owner's own password.
 *
 * The password is asked for on EVERY retrieval, not once per session: Laravel's
 * password.confirm middleware keeps a 15-minute session stamp, which would
 * authorise unlimited later downloads from an unattended browser — not what
 * "enter your password to download it" means.
 */
class VpnConfigAccess extends ModalComponent
{
    use ResolvesOperator;

    public string $uuid;

    public string $name = '';

    public string $password = '';

    /** Opaque handle; the plaintext never enters the component snapshot. */
    public ?string $token = null;

    public bool $showQr = false;

    public function mount(string $uuid): void
    {
        $peer = VpnPeer::query()->where('uuid', $uuid)->firstOrFail();
        $this->authorize('downloadConfig', $peer);

        $this->uuid = $uuid;
        $this->name = $peer->name;
    }

    /**
     * Every later request re-checks: once revealed, the plaintext sits in the
     * handoff cache for ten minutes, and an open modal would otherwise keep
     * handing it out after the access was revoked or reassigned — exactly the
     * window purgeSecret() exists to close.
     */
    public function hydrate(): void
    {
        $peer = VpnPeer::withTrashed()->where('uuid', $this->uuid)->first();

        if ($peer === null || Gate::denies('downloadConfig', $peer)) {
            ConfigHandoff::forget($this->token);
            $this->token = null;
            abort(403);
        }
    }

    public function reveal(): void
    {
        // A retry must not still be showing the previous attempt's complaint.
        $this->resetErrorBag('password');

        $peer = VpnPeer::query()->where('uuid', $this->uuid)->firstOrFail();
        $this->authorize('downloadConfig', $peer);

        if (! $operator = $this->currentOperator()) {
            return;
        }

        // Rate-limited per user, so a stolen session cannot grind the password.
        $key = 'vpn-config:'.$operator->id;
        if (RateLimiter::tooManyAttempts($key, 5)) {
            $this->addError('password', __('vpn.too_many_attempts', ['seconds' => RateLimiter::availableIn($key)]));

            return;
        }

        if (! Hash::check($this->password, $operator->password)) {
            RateLimiter::hit($key, 300);
            Log::warning('VPN config download refused: wrong password', [
                'user_id' => $operator->id, 'peer' => $peer->uuid,
            ]);
            $this->addError('password', __('vpn.wrong_password'));

            return;
        }

        RateLimiter::clear($key);

        $plaintext = ConfigVault::decrypt((string) $peer->config_secret);
        if ($plaintext === null) {
            // Wrong or rotated key, or a tampered record. Never guess.
            $this->addError('password', __('vpn.config_unreadable'));

            return;
        }

        // Counted in the database, not read-modify-written here: two concurrent
        // retrievals would otherwise record one. This is an audit trail, so
        // under-reporting is the one failure mode it must not have.
        VpnPeer::query()->whereKey($peer->getKey())->update([
            'last_downloaded_at' => now(),
            'download_count' => DB::raw('download_count + 1'),
        ]);

        Log::info('VPN config downloaded', ['user_id' => $operator->id, 'peer' => $peer->uuid]);

        $this->reset('password');
        $this->token = ConfigHandoff::put($plaintext);
    }

    public function toggleQr(): void
    {
        $this->showQr = ! $this->showQr;
    }

    public function filename(): string
    {
        return Str::limit(Str::slug($this->name) ?: 'clupilot', 15, '').'.conf';
    }

    public function render()
    {
        $config = ConfigHandoff::get($this->token);

        return view('livewire.admin.vpn-config-access', [
            'config' => $config,
            'qrSvg' => $config !== null && $this->showQr ? QrCode::svg($config) : null,
        ]);
    }
}
