<?php

namespace App\Support;

use App\Models\Auth\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

/**
 * La API de administración (`/api/admin/v1`, PRODSECU-215): sus permisos y el usuario que
 * es dueño de los tokens.
 *
 * ## Los permisos del token no son los del panel
 *
 * Son las *abilities* de Sanctum, guardadas en `personal_access_tokens.abilities`. Se
 * eligen al crear cada token y **no tienen nada que ver con Spatie**: el usuario de
 * servicio no tiene ningún rol. Así un token solo puede hacer lo que se le marcó, y lo que
 * pueda hacer una persona en el panel no se cuela por la API.
 *
 * ## Por qué un usuario de servicio y no quien crea el token
 *
 * Sanctum cuelga cada token de un modelo (`tokenable`). Si fuera la persona que lo crea, el
 * token dejaría de funcionar el día que esa persona se diera de baja, y la integración de
 * Sistemas se caería sin que nadie hubiera tocado nada. Quién lo creó queda en el log del
 * panel (16048).
 *
 * El usuario no puede entrar en el panel: no tiene rol y su contraseña es aleatoria y no se
 * guarda en ningún sitio.
 */
class ApiDeAdministracion
{
    public const EMAIL_DEL_USUARIO = 'api-admin@manager.tresipunt.internal';

    public const NOMBRE_DEL_USUARIO = 'API de administración (servicio)';

    /**
     * Las *abilities* que puede llevar un token, con su etiqueta para la pantalla.
     *
     * Lectura y escritura van separadas para poder dar un token que solo consulte.
     *
     * @var array<string, string>
     */
    public const PERMISOS = [
        'admin.catalogs.read' => 'Consultar plataformas, usos y productos',
        'admin.clients.read' => 'Buscar clientes',
        'admin.clients.write' => 'Crear y editar clientes (incluye buscarlos)',
        'admin.license-tokens.read' => 'Buscar licencias',
        'admin.license-tokens.write' => 'Crear y editar licencias y añadirles productos (incluye buscarlas)',
        'admin.environments.read' => 'Buscar entornos',
        'admin.environments.write' => 'Crear y editar entornos (incluye buscarlos)',
    ];

    /**
     * Los permisos pedidos más el `.read` de cada `.write`: las rutas de escritura exigen
     * los dos, porque responden con la ficha del registro.
     *
     * @param  list<string>  $permisos
     * @return list<string>
     */
    public static function conSusLecturas(array $permisos): array
    {
        $lecturas = array_map(
            fn (string $permiso) => preg_replace('/\.write$/', '.read', $permiso),
            array_filter($permisos, fn ($permiso) => is_string($permiso) && str_ends_with($permiso, '.write'))
        );

        return array_values(array_unique([...$permisos, ...$lecturas]));
    }

    /**
     * El dueño de todos los tokens de esta API. Se crea la primera vez que hace falta.
     */
    public static function usuarioDeServicio(): User
    {
        $usuario = User::withTrashed()->whereRaw('lower(email) = ?', [self::EMAIL_DEL_USUARIO])->first();

        // **No se adopta a una persona.** Si alguien creó a mano un usuario con este correo y
        // le dio un rol, ese usuario —con contraseña conocida— pasaría a ser el dueño de los
        // tokens. Se para aquí y se arregla desde Usuarios (auditoría PRODSECU-215).
        if ($usuario !== null && $usuario->roles()->exists()) {
            throw new \RuntimeException(
                'Ya hay un usuario con el correo ' . self::EMAIL_DEL_USUARIO . ' y con rol: no puede ser el '
                . 'dueño de los tokens de la API. Quítale el rol o cámbiale el correo desde Usuarios.'
            );
        }

        if ($usuario === null) {
            $usuario = new User();
            $usuario->name = self::NOMBRE_DEL_USUARIO;
            $usuario->email = self::EMAIL_DEL_USUARIO;
            $usuario->password = Hash::make(Str::random(64));
            $usuario->save();

            return $usuario;
        }

        if ($usuario->trashed()) {
            $usuario->restore();
        }

        return $usuario;
    }

    /**
     * ¿Es este el usuario de servicio? Para no enseñarlo ni dejar editarlo como una persona.
     */
    public static function esUsuarioDeServicio(?User $usuario): bool
    {
        // Sin distinguir mayúsculas: la colación de MySQL no las distingue al buscar, y un
        // `API-ADMIN@…` encontrado pero no reconocido dejaría todos los tokens en 403.
        return $usuario !== null && mb_strtolower((string) $usuario->email) === self::EMAIL_DEL_USUARIO;
    }
}
