<?php

namespace App\Livewire\Services;

use App\Livewire\Concerns\WithFleetContext;
use App\Services\FleetService;
use App\Support\Confirm\ConfirmToken;
use App\Support\Confirm\InvalidConfirmToken;
use Illuminate\Support\Str;
use Livewire\Attributes\Layout;
use Livewire\Attributes\On;
use Livewire\Component;
use Throwable;

#[Layout('layouts.app')]
class Index extends Component
{
    use WithFleetContext;

    /** Cap on retained journal rows so the live poll never grows the payload unbounded. */
    private const JOURNAL_MAX = 200;

    /** Active server name (from fleet context). */
    public string $server = '—';

    public bool $connected = false;

    public bool $ready = false;

    /** wire:model search over service name + description */
    public string $search = '';

    /** @var array<int, array{name:string,status:string,sub:string,enabled:bool,desc:string}> */
    public array $services = [];

    /** @var array<int, array{time:string,unit:string,level:string,text:string}> */
    public array $journal = [];

    /** journald cursor to resume the live poll from (null until the first read). */
    public ?string $journalCursor = null;

    public function mount(): void
    {
        $this->server = $this->activeServer()?->name ?? '—';
    }

    /** Lazy: the SSH read runs after the shell renders (wire:init), behind a skeleton. */
    public function load(FleetService $fleet): void
    {
        $active = $this->activeServer();

        if ($active && $active->credential_exists) {
            try {
                $data = $fleet->systemd($active);
                $this->services = $data['services'];
                $this->journal = $data['journal'];
                $this->journalCursor = $data['cursor'] ?? null;
                $this->connected = true;
            } catch (Throwable) {
                $this->connected = false;
            }
        }

        $this->ready = true;
    }

    /**
     * Live journal: poll for entries since the last cursor and APPEND them, so no line
     * is dropped between ticks (a true `journalctl -f` stream needs a PTY — deferred to
     * the web terminal). Caps the retained rows and survives a transient SSH hiccup.
     */
    public function pollJournal(FleetService $fleet): void
    {
        if (! $this->ready || ! $this->connected) {
            return;
        }

        $active = $this->activeServer();

        if (! $active || ! $active->credential_exists) {
            return;
        }

        try {
            // Fetch ceiling == display ceiling: never under-fill, never carry more than we show.
            $data = $fleet->journalSince($active, $this->journalCursor, self::JOURNAL_MAX);
        } catch (Throwable) {
            return; // keep the current view; retry on the next tick
        }

        if ($data['journal'] !== []) {
            $this->journal = array_slice(
                array_merge($this->journal, $data['journal']),
                -self::JOURNAL_MAX
            );
        }

        $this->journalCursor = $data['cursor'];
    }

    /**
     * Service control (R5): opens the wire-elements/modal confirm dialog. The
     * modal writes the AuditEvent and re-dispatches `serviceConfirmed`, which
     * applyService() handles. SSH exec (systemctl) is wired in behind this later.
     */
    public function confirm(string $op, int $index): void
    {
        abort_unless(auth()->user()?->can('operate'), 403);

        $name = $this->filteredServices[$index]['name'] ?? null;
        if ($name === null) {
            return;
        }

        [$heading, $phrase, $label, $action, $icon] = match ($op) {
            'start' => [__('services.confirm_start_heading'), __('services.confirm_start_phrase'), __('services.confirm_start_label'), 'service.start', 'power'],
            'stop' => [__('services.confirm_stop_heading'), __('services.confirm_stop_phrase'), __('services.confirm_stop_label'), 'service.stop', 'power'],
            default => [__('services.confirm_restart_heading'), __('services.confirm_restart_phrase'), __('services.confirm_restart_label'), 'service.restart', 'rotate'],
        };

        $this->dispatch('openModal',
            component: 'modals.confirm-action',
            arguments: [
                'heading' => $heading,
                'body' => __('services.confirm_body', ['name' => $name, 'phrase' => $phrase, 'server' => $this->server]),
                'confirmLabel' => $label,
                'danger' => $op === 'stop',
                'icon' => $icon,
                'notify' => __('services.confirm_notify', ['name' => $name, 'label' => $label]),
                'token' => ConfirmToken::issue(
                    'serviceConfirmed',
                    ['op' => $op, 'name' => $name],
                    $action,
                    "{$name} · {$this->server}",
                    $this->activeServer()?->id,
                ),
            ],
        );
    }

    /** Performs the confirmed systemctl action over SSH, then reloads real state. */
    #[On('serviceConfirmed')]
    public function applyService(string $confirmToken, FleetService $fleet): void
    {
        abort_unless(auth()->user()?->can('operate'), 403);

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

        $active = $this->activeServer();

        if (! $active || ($payload['serverId'] ?? null) !== $active->id || ! $active->credential_exists) {
            return; // wrong/absent server (retarget attempt) or no credential — no-op
        }

        try {
            $res = $fleet->serviceAction($active, $op, $name);
        } catch (Throwable $e) {
            $this->dispatch('notify', message: __('services.action_error', ['name' => $name, 'error' => $e->getMessage()]));

            return;
        }

        $this->dispatch('notify', message: $res['ok']
            ? __('services.action_done', ['name' => $name, 'op' => $op])
            : __('services.action_failed', ['name' => $name, 'output' => Str::limit($res['output'] ?: __('services.no_permission'), 90)]));

        try {
            $this->services = $fleet->systemd($active)['services'];
        } catch (Throwable) {
            // keep current list on reload failure
        }
    }

    /** @return array<int, array{name:string,status:string,sub:string,enabled:bool,desc:string}> */
    public function getFilteredServicesProperty(): array
    {
        $q = trim(mb_strtolower($this->search));

        if ($q === '') {
            return $this->services;
        }

        return array_values(array_filter(
            $this->services,
            fn (array $s): bool => str_contains(mb_strtolower($s['name']), $q)
                || str_contains(mb_strtolower($s['desc']), $q)
        ));
    }

    public function render()
    {
        return view('livewire.services.index')->title(__('services.title'));
    }
}
