<?php

namespace App\Livewire\Files;

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

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

    /** Current working directory in the SFTP browser. */
    public string $path = '/';

    public bool $connected = false;

    public bool $ready = false;

    public $upload = null;

    /** @var array<int, array{name:string,type:string,size:?int,perms:string,owner:string,modified:string}> */
    public array $entries = [];

    public function mount(): void
    {
        // listing loads lazily via wire:init -> load()
    }

    /** Read the active server's directory over SSH; empty + offline on failure. */
    public function load(): void
    {
        $this->entries = [];
        $this->connected = false;

        $active = $this->activeServer();
        if ($active && $active->credential_exists) {
            try {
                $this->entries = app(FleetService::class)->files($active, $this->path);
                $this->connected = true;
            } catch (Throwable) {
                $this->connected = false;
            }
        }

        $this->ready = true;
    }

    public function open(int $index): void
    {
        $name = $this->entries[$index]['name'] ?? null;
        if ($name === null) {
            return;
        }

        // basename() for parity with download()/edit()/confirmDelete(): a listing entry
        // name is a single path segment, never a traversal fragment. $entries is a public,
        // non-#[Locked] property, so a crafted /livewire/update could otherwise set name to
        // ../.. and walk outside the browsed tree.
        $this->path = rtrim($this->path, '/').'/'.basename($name);
        $this->load();
    }

    public function go(int $index): void
    {
        $path = $this->crumbs[$index]['path'] ?? '/';
        $this->path = $path !== '' ? $path : '/';
        $this->load();
    }

    public function up(): void
    {
        $this->path = dirname($this->path) ?: '/';
        $this->load();
    }

    /** Upload the selected file into the current directory (SFTP put). */
    public function updatedUpload(FleetService $fleet): void
    {
        abort_unless(auth()->user()?->can('operate'), 403);

        $this->validate(['upload' => ['file', 'max:51200']]);  // 50 MB

        $active = $this->activeServer();
        if ($active && $active->credential_exists && $this->upload) {
            try {
                $name = basename($this->upload->getClientOriginalName());
                $fleet->uploadFile($active, rtrim($this->path, '/').'/'.$name, $this->upload->getRealPath());
                $this->dispatch('notify', message: __('files.uploaded', ['name' => $name]));
            } catch (Throwable $e) {
                $this->dispatch('notify', message: __('files.upload_failed', ['error' => $e->getMessage()]));
            }
        }

        $this->reset('upload');
        $this->load();
    }

    /** Stream a remote file to the browser as a download (SFTP get). */
    public function download(int $index, FleetService $fleet)
    {
        // Reading a file's BYTES over the server credential (often root) discloses secrets a
        // viewer must not reach (/etc/shadow, keys, .env). Browsing the LISTING stays open;
        // pulling content requires operate, like every other file operation.
        abort_unless(auth()->user()?->can('operate'), 403);

        $name = $this->entries[$index]['name'] ?? null;
        $active = $this->activeServer();
        if ($name === null || ! $active || ! $active->credential_exists) {
            return null;
        }

        try {
            $content = $fleet->getFile($active, rtrim($this->path, '/').'/'.basename($name));
        } catch (Throwable $e) {
            $this->dispatch('notify', message: __('files.download_failed', ['error' => $e->getMessage()]));

            return null;
        }

        return response()->streamDownload(fn () => print ($content), basename($name));
    }

    /** Open the view/edit modal for a file. */
    public function edit(int $index): void
    {
        // The editor reads (and can write) file content — an operate action. Gate here so a
        // viewer never even opens the modal; FileEditor::load() re-checks on the read itself.
        abort_unless(auth()->user()?->can('operate'), 403);

        $name = $this->entries[$index]['name'] ?? null;
        $active = $this->activeServer();
        if ($name === null || ! $active) {
            return;
        }

        $this->dispatch('openModal',
            component: 'modals.file-editor',
            arguments: [
                'serverId' => $active->id,
                'path' => rtrim($this->path, '/').'/'.basename($name),
                'name' => basename($name),
            ],
        );
    }

    #[On('fileSaved')]
    public function reloadAfterSave(): void
    {
        $this->load();
    }

    /**
     * Breadcrumb segments derived from $path (root first, then each folder).
     *
     * @return array<int, array{label: string, path: string}>
     */
    public function getCrumbsProperty(): array
    {
        $crumbs = [['label' => '/', 'path' => '/']];
        $acc = '';

        foreach (array_filter(explode('/', $this->path)) as $segment) {
            $acc .= '/'.$segment;
            $crumbs[] = ['label' => $segment, 'path' => $acc];
        }

        return $crumbs;
    }

    /**
     * Delete a file (R5): opens the confirm modal, which writes the AuditEvent
     * and re-dispatches `fileConfirmed` — handled by deleteEntry(), which performs
     * the SFTP unlink over the active server's connection.
     */
    public function confirmDelete(int $index): void
    {
        abort_unless(auth()->user()?->can('operate'), 403);

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

        // Seal the FULL path + the server id so the token cannot be retargeted to
        // another directory (client-held $path) or host (session-selected server).
        $fullPath = rtrim($this->path, '/')."/{$name}";

        $this->dispatch('openModal',
            component: 'modals.confirm-action',
            arguments: [
                'heading' => __('files.delete_heading'),
                'body' => __('files.delete_body', ['name' => $name, 'path' => $this->path]),
                'confirmLabel' => __('common.delete'),
                'danger' => true,
                'icon' => 'trash',
                'notify' => __('files.delete_notify', ['name' => $name]),
                'token' => ConfirmToken::issue(
                    'fileConfirmed',
                    ['path' => $fullPath],
                    'file.delete',
                    $fullPath,
                    $this->activeServer()?->id,
                ),
            ],
        );
    }

    /** Applies the confirmed deletion over SFTP, then reloads the directory. */
    #[On('fileConfirmed')]
    public function deleteEntry(string $confirmToken, FleetService $fleet): void
    {
        abort_unless(auth()->user()?->can('operate'), 403);

        try {
            $payload = ConfirmToken::consume($confirmToken, 'fileConfirmed');
        } catch (InvalidConfirmToken) {
            return; // forged / replayed / direct-bypass attempt — no-op
        }
        $active = $this->activeServer();

        if (! $active || ($payload['serverId'] ?? null) !== $active->id) {
            return; // token sealed for a different server — refuse to retarget
        }

        // Delete the SEALED path, never a client-recombined one.
        $path = $payload['params']['path'];

        if ($active->credential_exists) {
            try {
                $fleet->deleteFile($active, $path);
            } catch (Throwable $e) {
                $this->dispatch('notify', message: __('files.delete_failed', ['error' => $e->getMessage()]));

                return;
            }
        }

        $this->load();
    }

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