<?php

namespace App\Livewire\Servers;

use App\Models\AuditEvent;
use App\Models\Server;
use App\Services\Fail2banService;
use App\Services\FirewallService;
use App\Services\FleetService;
use App\Services\HardeningService;
use App\Support\Confirm\ConfirmToken;
use App\Support\Confirm\InvalidConfirmToken;
use App\Support\Os\OsDetector;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Livewire\Attributes\Layout;
use Livewire\Attributes\On;
use Livewire\Component;
use Throwable;

#[Layout('layouts.app')]
class Show extends Component
{
    /** Route-model-bound by uuid (R11). */
    public Server $server;

    /** Page <title>; localized at runtime (attributes can't call __()). */
    public function title(): string
    {
        return __('servers.show_title');
    }

    public bool $connected = false;

    public bool $ready = false;

    public float $loadAvg = 0;

    public array $volumes = [];

    public array $interfaces = [];

    public array $hardening = [];

    /** Firewall state (installed/active/defaults/rules) for the Firewall-Regeln panel. */
    public array $firewall = [];

    /** fail2ban state (jails/banned IPs/whitelist) for the fail2ban-Status panel. */
    public array $fail2ban = [];

    /** Input for adding an IP/CIDR to the fail2ban whitelist. */
    public string $newIgnoreIp = '';

    /** Detected OS tooling (family/package manager/firewall) for the identity panel. */
    public array $os = [];

    public array $sshKeys = [];

    /**
     * Pull a live snapshot over SSH and persist identity/metrics onto the row.
     * On any failure (no credential, unreachable) the page renders an offline
     * state from the last persisted values instead of crashing.
     */
    public function mount(): void
    {
        // Viewing a server's details makes it the ACTIVE server, so the sidebar switcher
        // (and the scoped sections) reflect the server on screen. Set ONLY at mount — i.e.
        // once per deliberate navigation — so a slow in-flight load() or a parallel tab
        // can never write a stale value over a newer selection (the single-session active
        // server is last-writer-wins; we keep writes to deliberate navigations only).
        session(['active_server_id' => $this->server->id]);

        // identity + gauges render from the persisted row immediately;
        // the live snapshot loads lazily via wire:init -> load().
    }

    /** Lazy: pull the full SSH snapshot after the shell renders, behind skeletons. */
    public function load(FleetService $fleet): void
    {
        try {
            $snap = $fleet->snapshot($this->server);
            $id = $snap['identity'];
            $rootUsed = collect($snap['volumes'])->firstWhere('mount', '/')['used'] ?? $this->server->disk;

            $this->server->forceFill([
                'cpu' => $snap['metrics']['cpu'],
                'mem' => $snap['metrics']['mem'],
                'disk' => $rootUsed,
                'os' => $id['os'],
                'hostname' => $id['hostname'],
                'uptime' => $id['uptime'],
                'status' => 'online',
                'last_seen_at' => now(),
                'specs' => [
                    'cores' => $id['cores'],
                    'ram_gb' => $id['ram_gb'],
                    'disk_gb' => $id['disk_gb'],
                    'arch' => $id['arch'],
                    'kernel' => $id['kernel'],
                    'virt' => $id['virt'],
                ],
            ])->save();

            $this->volumes = $snap['volumes'];
            $this->interfaces = $snap['interfaces'];
            $this->sshKeys = $snap['sshKeys'];

            // Detected OS profile (cached) — surfaces the package manager + firewall
            // tooling and drives which hardening features are offered.
            try {
                // fresh probe on each full load; primes the 1h cache the hardening,
                // firewall and update reads reuse within this request cycle.
                $profile = app(OsDetector::class)->detect($this->server, fresh: true);
                $this->os = [
                    'familyLabel' => $profile->familyLabel(),
                    'packageManager' => $profile->packageManager,
                    'firewallTool' => $profile->firewallTool,
                    'serviceManager' => $profile->serviceManager,
                ];
            } catch (Throwable) {
                $this->os = [];
            }

            // Hardening state is a separate PRIVILEGED read (sshd -T + pkg checks).
            try {
                $this->hardening = app(HardeningService::class)->state($this->server);
            } catch (Throwable) {
                $this->hardening = [];
            }

            // Firewall rules/defaults (separate PRIVILEGED read; empty on failure).
            try {
                $this->firewall = app(FirewallService::class)->status($this->server);
            } catch (Throwable) {
                $this->firewall = [];
            }

            // fail2ban jails/banned IPs/whitelist (separate PRIVILEGED read).
            try {
                $this->fail2ban = app(Fail2banService::class)->status($this->server);
            } catch (Throwable) {
                $this->fail2ban = [];
            }
            $this->loadAvg = (float) ($snap['metrics']['load'] ?? 0);
            $this->connected = true;
        } catch (Throwable) {
            $this->connected = false;
            if ($this->server->status !== 'offline') {
                $this->server->forceFill(['status' => 'offline'])->save();
            }
        }

        $this->ready = true;
    }

    /** Refresh the live gauges from the poller-updated row (wire:poll). */
    public function pollMetrics(): void
    {
        $this->server->refresh();
    }

    /**
     * Revoke an SSH key (R5): opens the confirm modal, which writes the
     * AuditEvent and re-dispatches `keyRemoved`. SSH layer removal lands later.
     */
    public function confirmKeyRemoval(int $index): void
    {
        abort_unless(auth()->user()?->can('manage-fleet'), 403);

        $key = $this->sshKeys[$index] ?? null;
        if ($key === null) {
            return;
        }

        $fingerprint = $key['fingerprint'];
        $comment = $key['comment'];

        $this->dispatch('openModal',
            component: 'modals.confirm-action',
            arguments: [
                'heading' => __('servers.confirm_remove_key_heading'),
                'body' => __('servers.confirm_remove_key_body', ['name' => $comment, 'server' => $this->server->name]),
                'confirmLabel' => __('common.remove'),
                'danger' => true,
                'icon' => 'trash',
                'notify' => __('servers.notify_key_removed', ['name' => $comment]),
                'token' => ConfirmToken::issue(
                    'keyRemoved',
                    ['fingerprint' => $fingerprint],
                    'ssh_key.remove',
                    "{$comment} · {$this->server->name}",
                    $this->server->id,
                ),
            ],
        );
    }

    /** Applies the confirmed key removal over SSH, then reloads the list. */
    #[On('keyRemoved')]
    public function removeKey(string $confirmToken, FleetService $fleet): void
    {
        // Re-gate on consume: ConfirmToken is uid-bound, not role-bound, so a token issued
        // while the user could manage the fleet must still be refused after a demotion.
        abort_unless(auth()->user()?->can('manage-fleet'), 403);

        try {
            $payload = ConfirmToken::consume($confirmToken, 'keyRemoved');
        } catch (InvalidConfirmToken) {
            return; // forged / replayed / direct-bypass attempt — no-op
        }
        if (($payload['serverId'] ?? null) !== $this->server->id) {
            return; // token sealed for a different server — refuse to retarget
        }
        $fingerprint = $payload['params']['fingerprint'];

        try {
            $fleet->removeAuthorizedKey($this->server, $fingerprint);
        } catch (Throwable $e) {
            $this->dispatch('notify', message: __('servers.notify_remove_failed', ['error' => $e->getMessage()]));

            return;
        }

        $this->reloadKeys($fleet);
    }

    /** Re-read the authorized keys after an add/remove. */
    #[On('keyChanged')]
    public function reloadKeys(FleetService $fleet): void
    {
        try {
            $this->sshKeys = $fleet->sshKeys($this->server);
        } catch (Throwable) {
            // keep the current list on failure
        }
    }

    /** Credential changed -> reconnect with the new login and re-pull the snapshot. */
    #[On('credentialChanged')]
    public function reloadAfterCredential(FleetService $fleet): void
    {
        $this->server->refresh();
        $this->ready = false;
        $this->load($fleet);
    }

    /** Open the file manager scoped to this server. */
    public function openFiles()
    {
        session(['active_server_id' => $this->server->id]);

        return $this->redirect(route('files.index'), navigate: true);
    }

    /**
     * Toggle the credential between aktiv/gesperrt (sets/clears disabled_at).
     * The CredentialVault already refuses a disabled credential, so this is the
     * operator's kill-switch for a deposited login without deleting it.
     */
    public function toggleCredential(): void
    {
        abort_unless(auth()->user()?->can('manage-fleet'), 403);

        $cred = $this->server->credential;
        if (! $cred) {
            return;
        }

        $disable = ! $cred->disabled;
        $cred->forceFill(['disabled_at' => $disable ? now() : null])->save();

        AuditEvent::create([
            'user_id' => Auth::id(),
            'server_id' => $this->server->id,
            'actor' => Auth::user()?->name ?? 'system',
            'action' => $disable ? 'credential.disable' : 'credential.enable',
            'target' => $this->server->name.' · '.$cred->username,
            'ip' => request()->ip(),
        ]);

        $this->server->refresh();
        $this->dispatch('notify', message: $disable ? __('servers.notify_access_locked') : __('servers.notify_access_unlocked'));
    }

    /**
     * Delete the credential (R5): opens the confirm modal, which writes the
     * AuditEvent and re-dispatches `credentialDeleted`.
     */
    public function confirmDeleteCredential(): void
    {
        abort_unless(auth()->user()?->can('manage-fleet'), 403);

        $cred = $this->server->credential;
        if (! $cred) {
            return;
        }

        $this->dispatch('openModal',
            component: 'modals.confirm-action',
            arguments: [
                'heading' => __('servers.confirm_delete_credential_heading'),
                'body' => __('servers.confirm_delete_credential_body', ['server' => $this->server->name]),
                'confirmLabel' => __('common.delete'),
                'danger' => true,
                'icon' => 'trash',
                'notify' => __('servers.notify_access_deleted'),
                'token' => ConfirmToken::issue(
                    'credentialDeleted',
                    [],
                    'credential.delete',
                    $this->server->name.' · '.$cred->username,
                    $this->server->id,
                ),
            ],
        );
    }

    /** Applies the confirmed credential deletion, then refreshes the row. */
    #[On('credentialDeleted')]
    public function deleteCredential(string $confirmToken): void
    {
        // Re-gate on consume: ConfirmToken is uid-bound, not role-bound, so a token issued
        // while the user could manage the fleet must still be refused after a demotion.
        abort_unless(auth()->user()?->can('manage-fleet'), 403);

        try {
            $payload = ConfirmToken::consume($confirmToken, 'credentialDeleted');
        } catch (InvalidConfirmToken) {
            return; // forged / replayed / direct-bypass attempt — no-op
        }
        if (($payload['serverId'] ?? null) !== $this->server->id) {
            return; // token sealed for a different server — refuse to retarget
        }

        $this->server->credential()->delete();
        $this->server->refresh();
    }

    /**
     * A hardening/firewall action was applied -> re-pull the snapshot so the
     * checklist + firewall row reflect the new state.
     */
    #[On('hardeningApplied')]
    public function reloadSnapshot(FleetService $fleet): void
    {
        $this->server->refresh();
        $this->ready = false;
        $this->load($fleet);
    }

    /**
     * Delete a firewall rule (R5): opens the confirm modal, which writes the
     * AuditEvent and re-dispatches `firewallRuleDelete` (handled below).
     */
    public function confirmDeleteRule(int $index): void
    {
        abort_unless(auth()->user()?->can('manage-network'), 403);

        $rule = $this->firewall['rules'][$index] ?? null;
        if ($rule === null) {
            return;
        }
        $spec = $rule['spec'] ?? '';
        $label = $rule['label'] ?? ($rule['raw'] ?? $spec);

        $this->dispatch('openModal',
            component: 'modals.confirm-action',
            arguments: [
                'heading' => __('servers.confirm_remove_rule_heading'),
                'body' => __('servers.confirm_remove_rule_body', ['label' => $label, 'server' => $this->server->name]),
                'confirmLabel' => __('common.remove'),
                'danger' => true,
                'icon' => 'trash',
                // No premature audit/notify — the handler records the real outcome
                // only after the remote command returns (audit reflects success).
                'notify' => '',
                'token' => ConfirmToken::issue(
                    'firewallRuleDelete',
                    ['spec' => $spec, 'label' => $label],
                    serverId: $this->server->id,
                ),
            ],
        );
    }

    /** Applies the confirmed rule deletion over SSH, then audits + reloads on success. */
    #[On('firewallRuleDelete')]
    public function deleteFirewallRule(string $confirmToken, FirewallService $firewall): void
    {
        // Re-gate on consume: ConfirmToken is uid-bound, not role-bound, so a token issued
        // while the user could manage the network must still be refused after a demotion.
        abort_unless(auth()->user()?->can('manage-network'), 403);

        try {
            $payload = ConfirmToken::consume($confirmToken, 'firewallRuleDelete');
        } catch (InvalidConfirmToken) {
            return; // forged / replayed / direct-bypass attempt — no-op
        }
        if (($payload['serverId'] ?? null) !== $this->server->id) {
            return; // token sealed for a different server — refuse to retarget
        }
        $spec = $payload['params']['spec'];
        $label = $payload['params']['label'];

        try {
            $res = $firewall->deleteRule($this->server, $spec);
        } catch (Throwable $e) {
            $this->dispatch('notify', message: __('servers.notify_remove_failed', ['error' => Str::limit($e->getMessage(), 90)]));

            return;
        }

        // The rule was already gone — inform, refresh, but do NOT audit a non-event.
        if (! empty($res['notFound'])) {
            $this->dispatch('notify', message: __('servers.notify_rule_not_found'));
            $this->reloadFirewall($firewall);

            return;
        }

        if (! $res['ok']) {
            $this->dispatch('notify', message: __('servers.notify_remove_failed', ['error' => Str::limit($res['output'] ?: __('servers.error_unknown'), 90)]));

            return;
        }

        AuditEvent::create([
            'user_id' => Auth::id(),
            'server_id' => $this->server->id,
            'actor' => Auth::user()?->name ?? 'system',
            'action' => 'firewall.rule_delete',
            'target' => $label.' · '.$this->server->name,
            'ip' => request()->ip(),
        ]);

        $this->dispatch('notify', message: __('servers.notify_firewall_rule_removed'));
        $this->reloadFirewall($firewall);
    }

    /** Re-read the firewall state after an add/delete/default change. */
    #[On('firewallChanged')]
    public function reloadFirewall(FirewallService $firewall): void
    {
        try {
            $this->firewall = $firewall->status($this->server);
        } catch (Throwable) {
            // keep the current state on failure
        }
    }

    /** Unban a fail2ban IP — recovery action, so a direct (audited) click, no R5 modal. */
    public function unbanIp(string $jail, string $ip, Fail2banService $fail2ban): void
    {
        abort_unless(auth()->user()?->can('manage-network'), 403);

        try {
            $res = $fail2ban->unban($this->server, $jail, $ip);
        } catch (Throwable $e) {
            $this->dispatch('notify', message: __('servers.notify_unban_failed', ['error' => Str::limit($e->getMessage(), 90)]));

            return;
        }
        if (! $res['ok']) {
            $this->dispatch('notify', message: __('servers.notify_unban_failed', ['error' => Str::limit($res['output'] ?: __('servers.error_unknown'), 90)]));

            return;
        }

        $this->audit('fail2ban.unban', $ip.' · '.$jail.' · '.$this->server->name);
        $this->dispatch('notify', message: __('servers.notify_ip_unbanned', ['ip' => $ip]));
        $this->reloadFail2ban($fail2ban);
    }

    /** Add an IP/CIDR to the fail2ban whitelist (ignoreip) — non-destructive, direct + audited. */
    public function addIgnore(Fail2banService $fail2ban): void
    {
        abort_unless(auth()->user()?->can('manage-network'), 403);

        $ip = trim($this->newIgnoreIp);
        if ($ip === '') {
            return;
        }

        try {
            $res = $fail2ban->addIgnoreIp($this->server, $ip);
        } catch (Throwable $e) {
            $this->dispatch('notify', message: __('servers.notify_whitelist_error', ['error' => Str::limit($e->getMessage(), 90)]));

            return;
        }
        if (! $res['ok']) {
            $this->dispatch('notify', message: __('servers.notify_whitelist_error', ['error' => Str::limit($res['output'] ?: __('servers.error_failed'), 90)]));

            return;
        }
        if (! empty($res['noop'])) {
            $this->newIgnoreIp = '';
            $this->dispatch('notify', message: $res['output']);

            return;
        }

        $this->audit('fail2ban.ignoreip_add', $ip.' · '.$this->server->name);
        $this->newIgnoreIp = '';
        $this->dispatch('notify', message: __('servers.notify_ip_whitelisted', ['ip' => $ip]));
        $this->reloadFail2ban($fail2ban);
    }

    /** Remove an IP/CIDR from the fail2ban whitelist — direct + audited (loopback is refused by the service). */
    public function removeIgnore(string $ip, Fail2banService $fail2ban): void
    {
        abort_unless(auth()->user()?->can('manage-network'), 403);

        try {
            $res = $fail2ban->removeIgnoreIp($this->server, $ip);
        } catch (Throwable $e) {
            $this->dispatch('notify', message: __('servers.notify_whitelist_error', ['error' => Str::limit($e->getMessage(), 90)]));

            return;
        }
        if (! $res['ok']) {
            $this->dispatch('notify', message: __('servers.notify_whitelist_error', ['error' => Str::limit($res['output'] ?: __('servers.error_failed'), 90)]));

            return;
        }
        if (! empty($res['noop'])) {
            $this->dispatch('notify', message: $res['output']);

            return;
        }

        $this->audit('fail2ban.ignoreip_remove', $ip.' · '.$this->server->name);
        $this->dispatch('notify', message: __('servers.notify_ip_unwhitelisted', ['ip' => $ip]));
        $this->reloadFail2ban($fail2ban);
    }

    /** Re-read fail2ban state after an unban/ban/whitelist change. */
    #[On('fail2banChanged')]
    public function reloadFail2ban(Fail2banService $fail2ban): void
    {
        try {
            $this->fail2ban = $fail2ban->status($this->server);
        } catch (Throwable) {
            // keep the current state on failure
        }
    }

    /** Write one AuditEvent for a fail2ban action (success path only). */
    private function audit(string $action, string $target): void
    {
        AuditEvent::create([
            'user_id' => Auth::id(),
            'server_id' => $this->server->id,
            'actor' => Auth::user()?->name ?? 'system',
            'action' => $action,
            'target' => $target,
            'ip' => request()->ip(),
        ]);
    }

    public function render()
    {
        return view('livewire.servers.show')->title($this->title());
    }
}
