<?php

namespace App\Livewire\Configurations;

use App\Models\Auth\User;
use App\Models\Monitoring\Log as MonitoringLog;
use App\Support\ApiDeAdministracion;
use Carbon\CarbonImmutable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rule;
use Laravel\Sanctum\PersonalAccessToken;
use Livewire\Component;

/**
 * Tokens de la API de administración — Configuración → API de administración
 * (PRODSECU-215).
 *
 * **Para qué.** Sistemas automatiza las altas de plataformas contra `/api/admin/v1`, y
 * cada script necesita una credencial. Aquí se emiten, se ven y se revocan, sin tocar la
 * base de datos ni pedírselo a nadie.
 *
 * **Cada token solo hace lo que se le marca.** Los permisos son las *abilities* de
 * Sanctum, no los roles del panel: ver `ApiDeAdministracion`. Todos cuelgan del usuario
 * de servicio para que no dejen de funcionar cuando quien los creó se va; quién lo creó
 * queda en el log (16048).
 *
 * **El token en claro se enseña una vez y no se guarda.** Sanctum solo guarda el hash, así
 * que después de crearlo no hay forma de recuperarlo: si se pierde, se revoca y se crea
 * otro. Por eso no va en una propiedad pública —esas viajan al navegador en cada petición
 * de Livewire dentro del *snapshot* y se quedarían ahí—, sino en una protegida que solo
 * vive en la petición que lo crea: se pinta en ese render y en el siguiente ya no existe.
 */
class ApiTokens extends Component
{
    /** El nombre del token: para quién o para qué es. */
    public string $nombre = '';

    /** @var array<int, string> Las *abilities* marcadas. */
    public array $permisos = [];

    /** Caducidad opcional, `Y-m-d`. Vacía = no caduca. */
    public string $caducidad = '';

    /**
     * El token recién creado, en claro. **Protegida a propósito**: ver el docblock de la
     * clase. Solo existe durante la petición que lo crea.
     *
     * @var array{nombre: string, token: string}|null
     */
    protected ?array $recienCreado = null;

    /**
     * El `permission:` de la ruta ya lo cubre; se repite por si alguien saca la ruta del
     * grupo al reorganizar (MGR-012).
     */
    public function mount(): void
    {
        $this->authorize('admin.api-tokens.index');
    }

    protected function rules(): array
    {
        return [
            'nombre' => ['required', 'string', 'max:255'],
            'permisos' => ['required', 'array', 'min:1'],
            // Solo las claves que existen: una *ability* inventada no la comprueba nadie y
            // daría la falsa impresión de que restringe algo.
            'permisos.*' => ['string', 'distinct', Rule::in(array_keys(ApiDeAdministracion::PERMISOS))],
            'caducidad' => ['nullable', 'date_format:Y-m-d', 'after:today'],
        ];
    }

    protected function messages(): array
    {
        return [
            'nombre.required' => 'Ponle un nombre que diga para qué es, p. ej. «Script de altas de Sistemas».',
            'permisos.required' => 'Marca al menos un permiso: un token sin permisos no puede hacer nada.',
            'permisos.min' => 'Marca al menos un permiso: un token sin permisos no puede hacer nada.',
            'permisos.*.in' => 'Hay un permiso que no existe. Recarga la página y vuelve a marcarlos.',
            'caducidad.after' => 'La caducidad tiene que ser una fecha futura.',
            'caducidad.date_format' => 'La caducidad no es una fecha válida.',
        ];
    }

    public function crear(): void
    {
        // El gate de la ruta solo protege la entrada a la pantalla; una llamada directa a
        // este método no pasa por él (MGR-005).
        $this->authorize('admin.api-tokens.create');

        $datos = $this->validate();

        // Se guardan en el orden del catálogo, no en el de los clics: así dos tokens con
        // los mismos permisos se leen igual en el listado y en el log. Y **escribir arrastra
        // leer**: la API lo exige en las rutas de escritura, así que un `.write` sin su
        // `.read` sería un token que no sirve para nada.
        $abilities = array_values(array_intersect(
            array_keys(ApiDeAdministracion::PERMISOS),
            ApiDeAdministracion::conSusLecturas($datos['permisos'])
        ));

        // Hasta el final del día elegido: quien pone «caduca el 31» espera que el 31 aún
        // funcione.
        $expiraEl = $datos['caducidad'] !== '' && $datos['caducidad'] !== null
            ? CarbonImmutable::createFromFormat('Y-m-d', $datos['caducidad'])->endOfDay()
            : null;

        $nuevo = ApiDeAdministracion::usuarioDeServicio()
            ->createToken($datos['nombre'], $abilities, $expiraEl);

        // **Nunca el token en claro**: el log lo lee cualquiera con acceso al visor, y con
        // el token se crean clientes y licencias.
        MonitoringLog::db(
            'info',
            '16048',
            'Token de la API de administración creado: «' . $datos['nombre'] . '»'
            . ' — permisos: ' . implode(', ', $abilities)
            . ' — caducidad: ' . ($expiraEl?->format('d/m/Y') ?? 'sin caducidad')
            . ' (por ' . (Auth::user()?->name ?? 'desconocido') . ')',
            'PersonalAccessToken',
            (string) $nuevo->accessToken->getKey()
        );

        $this->recienCreado = [
            'nombre' => $datos['nombre'],
            'token' => $nuevo->plainTextToken,
        ];

        $this->reset(['nombre', 'permisos', 'caducidad']);
    }

    /**
     * Cierra el aviso del token recién creado.
     *
     * No hace nada más: el token ya no está en ninguna propiedad, así que el render que
     * provoca esta llamada ya sale sin él. Existe para que el botón «Ya lo he guardado»
     * tenga a quién llamar.
     */
    public function cerrarAviso(): void
    {
    }

    public function revocar(int $id): void
    {
        $this->authorize('admin.api-tokens.destroy');

        $usuario = $this->usuarioDeServicio();

        // Solo los del usuario de servicio: esta pantalla no debe poder borrar tokens de
        // otro dueño aunque alguien cambie el id en la petición.
        $token = PersonalAccessToken::query()
            ->where('tokenable_type', $usuario?->getMorphClass() ?? User::class)
            ->where('tokenable_id', $usuario?->getKey() ?? 0)
            ->findOrFail($id);

        $nombre = $token->name;

        // Borrar la fila es la revocación de Sanctum: la siguiente petición con ese token
        // recibe un 401.
        $token->delete();

        MonitoringLog::db(
            'info',
            '16049',
            'Token de la API de administración revocado: «' . $nombre . '»'
            . ' (por ' . (Auth::user()?->name ?? 'desconocido') . ')',
            'PersonalAccessToken',
            (string) $id
        );

        session()->flash('success', 'Token «' . $nombre . '» revocado. Lo que lo use dejará de funcionar desde ya.');
    }

    /**
     * El usuario de servicio **sin crearlo**: mirar la pantalla no debería dar de alta a
     * nadie. Se crea al emitir el primer token.
     */
    private function usuarioDeServicio(): ?User
    {
        return User::withTrashed()->where('email', ApiDeAdministracion::EMAIL_DEL_USUARIO)->first();
    }

    /**
     * Los tokens emitidos, los más recientes primero.
     *
     * @return Collection<int, array<string, mixed>>
     */
    public function tokens(): Collection
    {
        $usuario = $this->usuarioDeServicio();

        if ($usuario === null) {
            return collect();
        }

        return $usuario->tokens()
            ->orderByDesc('created_at')
            ->orderByDesc('id')
            ->get()
            ->map(fn (PersonalAccessToken $token) => [
                'id' => $token->id,
                'nombre' => $token->name,
                // Las etiquetas, que es lo que se entiende. Si apareciera una *ability* que
                // ya no está en el catálogo, se enseña tal cual en vez de esconderla.
                'permisos' => collect($token->abilities ?? [])
                    ->map(fn (string $ability) => ApiDeAdministracion::PERMISOS[$ability] ?? $ability)
                    ->all(),
                'creado' => $token->created_at,
                'usado' => $token->last_used_at,
                'caduca' => $token->expires_at,
                'caducado' => $token->expires_at !== null && $token->expires_at->isPast(),
            ]);
    }

    public function render()
    {
        return view('livewire.configurations.api-tokens', [
            'tokens' => $this->tokens(),
            'catalogo' => ApiDeAdministracion::PERMISOS,
            'recienCreado' => $this->recienCreado,
        ])->layout('layouts.app');
    }
}
