<?php

namespace App\Livewire\System;

use App\Models\AuditEvent;
use App\Services\DeploymentService;
use App\Support\Confirm\ConfirmToken;
use App\Support\Confirm\InvalidConfirmToken;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Livewire\Attributes\Layout;
use Livewire\Attributes\On;
use Livewire\Component;

/**
 * System settings — dashboard-configurable Domain + TLS.
 *
 * Domain & TLS: the panel domain is EDITABLE here. Saving persists it (Setting
 * `panel_domain`, overriding the install-time APP_DOMAIN) and asks the operator to
 * restart the stack; on restart everything re-derives from it (app.url, the Reverb
 * client endpoint, Caddy's on-demand cert, the secure-cookie flag). Nothing switches
 * mid-session — that is what makes it safe. Bare-IP/HTTP access stays a recovery path.
 */
#[Layout('layouts.app')]
class Index extends Component
{
    /** Valid TLS termination modes. */
    public const TLS_MODES = ['caddy', 'external'];

    /** Effective panel domain (DB override or install-time APP_DOMAIN); '' = bare IP. */
    public string $domain = '';

    /** The domain form field (FQDN, or empty to clear the override). */
    public string $domainInput = '';

    /** True after a save — shows the "restart the stack to apply" notice. */
    public bool $restartPending = false;

    /** True once the operator clicked "restart now" — the host watcher is applying it. */
    public bool $restartRequested = false;

    public string $tlsMode = 'caddy';

    public function mount(DeploymentService $deployment): void
    {
        // Active = what the stack currently serves (snapshot); the form edits the PENDING
        // (configured) domain. They differ when a saved change awaits a restart.
        $this->domain = (string) ($deployment->domain() ?? '');
        $this->domainInput = (string) ($deployment->configuredDomain() ?? '');
        $this->restartPending = $deployment->restartPending();
        $this->restartRequested = $deployment->restartRequested();
        $this->tlsMode = $deployment->externalTls() ? 'external' : 'caddy';
    }

    /** @return array<string, array<int, string>> */
    private function domainRules(): array
    {
        // Empty clears the override (revert to install-time APP_DOMAIN / bare IP).
        return ['domainInput' => ['nullable', 'string', 'max:253', 'regex:'.DeploymentService::DOMAIN_REGEX]];
    }

    /** Validate, then confirm the domain change via the shared modal (audited there). */
    public function confirmDomain(DeploymentService $deployment): void
    {
        abort_unless(auth()->user()?->can('manage-panel'), 403);

        $this->validate($this->domainRules(), [
            'domainInput.regex' => __('system.domain_invalid'),
            'domainInput.max' => __('system.domain_invalid'),
        ]);

        $new = strtolower(trim($this->domainInput));

        if ($new === (string) ($deployment->configuredDomain() ?? '')) {
            return; // no change vs the already-configured (pending) domain
        }

        $this->dispatch('openModal',
            component: 'modals.confirm-action',
            arguments: [
                'heading' => __('system.change_domain_heading'),
                'body' => $new !== ''
                    ? __('system.change_domain_body', ['domain' => $new])
                    : __('system.clear_domain_body'),
                'confirmLabel' => __('system.change_domain_confirm'),
                'danger' => true,
                'icon' => 'globe',
                'notify' => '', // defer: we surface the restart notice instead of a toast
                'token' => ConfirmToken::issue(
                    'domainChanged',
                    ['domain' => $new],
                    'system.settings_updated',
                    'panel_domain='.($new !== '' ? $new : '(cleared)'),
                ),
            ],
        );
    }

    #[On('domainChanged')]
    public function applyDomain(string $confirmToken, DeploymentService $deployment): void
    {
        // Re-gate on consume: ConfirmToken is uid-bound, not role-bound, so a token issued
        // while the user could manage the panel must still be refused after a demotion.
        abort_unless(auth()->user()?->can('manage-panel'), 403);

        try {
            $payload = ConfirmToken::consume($confirmToken, 'domainChanged');
        } catch (InvalidConfirmToken) {
            return; // forged / replayed / direct-bypass attempt — no-op
        }
        $domain = strtolower(trim($payload['params']['domain']));

        // Defensive re-validation (the value round-trips through the modal event).
        if ($domain !== '' && ! preg_match(DeploymentService::DOMAIN_REGEX, $domain)) {
            return;
        }

        $pending = $deployment->setDomain($domain);
        // The active (served) domain stays frozen until restart; only the pending changes.
        $this->domainInput = (string) ($pending ?? '');
        $this->restartPending = $deployment->restartPending();
        $this->dispatch('notify', message: __('system.domain_saved_notify'));
    }

    /**
     * One-click restart: write the sentinel and let the HOST watcher restart the stack.
     * The container never touches the Docker socket — it only requests via a marker file
     * the scoped systemd .path unit reacts to (see docker/restart-sentinel/). Kept explicit
     * (a button, not auto-fired on save) so the operator controls the timing.
     */
    public function restartNow(DeploymentService $deployment): void
    {
        abort_unless(auth()->user()?->can('manage-panel'), 403);

        // Surface a failed sentinel write instead of a "restarting" state that never resolves.
        if (! $deployment->requestRestart()) {
            $this->dispatch('notify', message: __('system.restart_write_failed'), level: 'error');

            return;
        }
        $this->restartRequested = true;
        $this->dispatch('notify', message: __('system.restart_running'));
    }

    /**
     * Operator-triggered Let's-Encrypt request for the ACTIVE, Caddy-served domain: a DNS
     * pre-check + an on-demand-TLS handshake (DeploymentService::requestCertificate). Per-user
     * throttled (5 / 10 min, auto-expiring — never a control-plane lockout) so it can't hammer
     * ACME, and audited. Only meaningful in caddy mode with an active domain.
     */
    public function requestCertificate(DeploymentService $deployment): void
    {
        abort_unless(auth()->user()?->can('manage-panel'), 403);

        $domain = $deployment->domain();

        if ($domain === null || $deployment->externalTls()) {
            $this->dispatch('notify', message: __('system.cert_not_applicable'), level: 'error');

            return;
        }

        $key = 'cert-request:'.Auth::id();
        if (RateLimiter::tooManyAttempts($key, 5)) {
            $this->dispatch('notify', message: __('system.cert_throttled', ['seconds' => RateLimiter::availableIn($key)]), level: 'error');

            return;
        }
        RateLimiter::hit($key, 600);

        $result = $deployment->requestCertificate($domain);

        AuditEvent::create([
            'user_id' => Auth::id(),
            'actor' => Auth::user()?->name ?? 'system',
            'action' => 'tls.cert_request',
            'target' => $domain.' → '.$result['status'],
            'ip' => request()->ip(),
        ]);

        [$message, $level] = match ($result['status']) {
            'issued' => [__('system.cert_issued', ['domain' => $domain]), 'success'],
            'dns_mismatch' => [__('system.cert_dns_mismatch', [
                'resolved' => implode(', ', $result['resolved'] ?? []) ?: '—',
                'server' => $result['serverIp'] ?? '?',
            ]), 'error'],
            default => [__('system.cert_failed'), 'error'],
        };

        $this->dispatch('notify', message: $message, level: $level);
    }

    /** TLS-mode changes are audited (confirmation via the shared modal). */
    public function confirmTlsMode(string $mode): void
    {
        abort_unless(auth()->user()?->can('manage-panel'), 403);

        if (! in_array($mode, self::TLS_MODES, true) || $mode === $this->tlsMode) {
            return;
        }

        $this->dispatch('openModal',
            component: 'modals.confirm-action',
            arguments: [
                'heading' => __('system.change_tls_heading'),
                'body' => $mode === 'external'
                    ? __('system.change_tls_body_external')
                    : __('system.change_tls_body_caddy'),
                'confirmLabel' => __('system.change_tls_confirm'),
                'danger' => true,
                'icon' => 'shield',
                'notify' => '',
                'token' => ConfirmToken::issue(
                    'tlsModeChanged',
                    ['mode' => $mode],
                ),
            ],
        );
    }

    #[On('tlsModeChanged')]
    public function applyTlsMode(string $confirmToken, DeploymentService $deployment): void
    {
        // Re-gate on consume: ConfirmToken is uid-bound, not role-bound, so a token issued
        // while the user could manage the panel must still be refused after a demotion.
        abort_unless(auth()->user()?->can('manage-panel'), 403);

        try {
            $payload = ConfirmToken::consume($confirmToken, 'tlsModeChanged');
        } catch (InvalidConfirmToken) {
            return; // forged / replayed / direct-bypass attempt — no-op
        }
        $mode = $payload['params']['mode'];

        if (! in_array($mode, self::TLS_MODES, true)) {
            return;
        }

        $deployment->setTlsMode($mode);
        $this->tlsMode = $mode;

        AuditEvent::create([
            'user_id' => Auth::id(),
            'actor' => Auth::user()?->name ?? 'system',
            'action' => 'system.settings_updated',
            'target' => 'tls_mode='.$mode,
            'ip' => request()->ip(),
        ]);

        $this->restartPending = true;
        $this->dispatch('notify', message: __('system.tls_saved_notify'));
    }

    public function render(DeploymentService $deployment)
    {
        return view('livewire.system.index', [
            'hasTls' => $this->domain !== '',
            'panelUrl' => $deployment->panelUrl(),
            'isOverridden' => $deployment->domainIsOverridden(),
            // Cert request is meaningful only when Caddy serves TLS for an ACTIVE domain.
            'showCert' => $this->tlsMode === 'caddy' && $this->domain !== '',
            'certStatus' => $deployment->certStatus(),
        ])->title(__('system.title'));
    }
}
