<?php

namespace App\Models\Products;

use App\Models\Auth\User;
use App\Models\Clients\Client;
use App\Models\Environments\Environment;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class LicenseToken extends Model
{
    use HasFactory, SoftDeletes;

    protected $table = 'license_tokens';

    protected $fillable = [
        'name',
        'client_id',
        'created_by',
        'token',
        'active',
        'start_at',
        'end_at',
        'mode',
        'rate_limit_per_minute',
        'metadata',
        'observation',
    ];

    protected $casts = [
        'deactivated_at' => 'datetime',
        'active'   => 'boolean',
        'start_at' => 'datetime',
        'end_at'   => 'datetime',
        'metadata' => 'array',
    ];

    /*
    |--------------------------------------------------------------------------
    | Relaciones
    |--------------------------------------------------------------------------
    */

    /**
     * Cliente propietario del token.
     */
    public function client()
    {
        return $this->belongsTo(Client::class);
    }

    /**
     * Usuario que creó el token (auditoría).
     */
    /**
     * Quién apagó la licencia a mano.
     *
     * Nulo mientras esté encendida, y también en las que se apagaron antes de que
     * existieran estas columnas: **la ficha tiene que aguantar que falte**.
     */
    public function deactivatedBy()
    {
        return $this->belongsTo(\App\Models\Auth\User::class, 'deactivated_by');
    }

    public function creator()
    {
        return $this->belongsTo(User::class, 'created_by');
    }

    /**
     * Productos asociados al token (pivot license_token_product).
     */
    public function products()
    {
        return $this->belongsToMany(
            Product::class,
            'license_token_product',
            'license_token_id',
            'product_id'
        )
            // **Sin `using()` a propósito.** Ese método exige que el pivote extienda
            // `Pivot` y `LicenseTokenProduct` extiende `Model` —cambiarlo arrastra el
            // `$incrementing` y los timestamps del pivote, y esta tabla sí tiene `id`
            // autoincremental—. No hace falta: las bajas y las restauraciones se hacen
            // con el modelo directamente, que es quien tiene el `SoftDeletes`.
            // **Y esto es lo que hace que el borrado en blando signifique algo.**
            // `belongsToMany` no sabe nada de `deleted_at`: sin este filtro, un
            // producto quitado seguiría dando servicio por la API, que es peor que no
            // tener borrado en blando. Toda la API consulta por esta relación —los
            // nueve `$licenseToken->products()` del middleware y las dos acciones—, así
            // que filtrar aquí las cubre todas.
            ->wherePivotNull('deleted_at')
            ->withPivot([
            'mode',
            'status',
            'start_at',
            'end_at',
            'observation',
            'assigned_by',
            'deleted_at',
        ])->withTimestamps();
    }

    /**
     * Entornos asociados a este token (1 token → N environments).
     */
    public function environments()
    {
        return $this->hasMany(Environment::class);
    }

    /*
    |--------------------------------------------------------------------------
    | Helpers
    |--------------------------------------------------------------------------
    */

    /**
     * El token recortado para caber en una tabla.
     *
     * Los tokens llegan a **36 caracteres** y no tienen un formato fijo: `DEMO-…`,
     * `TRIAL-…`, `ALPHA-…`, `3IP-…-…-…-…`. Un `Str::limit()` por la izquierda deja
     * varios indistinguibles entre sí, porque el prefijo es justo la parte que
     * comparten.
     *
     * Así que se conserva **el prefijo hasta el primer guion más cuatro caracteres** —lo
     * que identifica de qué familia y de qué cliente es— **y los cuatro últimos**, que
     * son los que distinguen dos tokens del mismo cliente. En medio, puntos suspensivos.
     *
     * **El completo solo está en el botón de copiar y en la ficha de la licencia.** Estuvo
     * también en el `title` de cada celda y se quitó el 2026-09-11: un tooltip se dispara al
     * pasar por encima y sale en cualquier captura o pantalla compartida, y para saber de qué
     * licencia es una fila no hace falta la credencial. Esto es para leer, no para usar.
     */
    public function getTokenCortoAttribute(): string
    {
        $completo = (string) $this->token;

        if (mb_strlen($completo) <= 18) {
            return $completo;
        }

        $guion = mb_strpos($completo, '-');
        $cabeza = $guion === false ? 8 : min($guion + 5, 12);

        return mb_substr($completo, 0, $cabeza) . '…' . mb_substr($completo, -4);
    }

    /**
     * Apaga la licencia dejando dicho quién y por qué.
     *
     * **Está aquí y no en la modal a propósito**, y las tres columnas del apagado **no
     * van en `$fillable`**: un `update(['active' => false])` desde cualquier formulario
     * se saltaría el motivo y el responsable, que es exactamente lo que pasaba antes.
     * Con la asignación directa, el único camino para apagar una licencia es este.
     *
     * Es la misma decisión que en `Client::darDeBaja()`.
     */
    public function apagar(string $motivo, ?int $usuarioId = null): void
    {
        $this->active = false;
        $this->deactivated_at = now();
        $this->deactivated_by = $usuarioId;
        $this->deactivation_reason = $motivo;
        $this->save();
    }

    /**
     * Vuelve a encender la licencia.
     *
     * Limpia el registro del apagado: la ficha diría «apagada por…» de una licencia
     * encendida. El rastro de que ocurrió queda en el log del panel, que es su sitio.
     *
     * **Encenderla no la revive si está caducada**: el middleware mira las dos cosas.
     * Devuelve si sigue caducada para que quien llame pueda decirlo.
     */
    public function encender(): bool
    {
        $this->active = true;
        $this->deactivated_at = null;
        $this->deactivated_by = null;
        $this->deactivation_reason = null;
        $this->save();

        return $this->end_at !== null && $this->end_at->copy()->startOfDay()->isPast();
    }

    public function isActive(): bool
    {
        return $this->active === true;
    }

    /**
     * Estado de esta licencia, en una palabra.
     *
     * Cuatro valores: `desactivada`, `vencida`, `pronto`, `activa`. Es el mismo
     * vocabulario que usa la ficha del cliente y el listado, y **vive aquí** para que
     * las tres pantallas no lo calculen cada una a su manera —que es como se acaba
     * pintando «vence pronto» y «vencida» a la vez para la misma licencia (MGR-025)—.
     *
     * El orden de las comprobaciones importa: una licencia desactivada **no se mira si
     * vence**, porque ya no da servicio. Y `end_at` a null significa «sin caducidad»,
     * no «caducada».
     */
    public function estado(): string
    {
        if (! $this->active) {
            return 'desactivada';
        }

        if (! $this->end_at) {
            return 'activa';
        }

        $fin = \Carbon\Carbon::parse($this->end_at);

        if ($fin->lessThanOrEqualTo(now())) {
            return 'vencida';
        }

        // Rango **cerrado**: de hoy a un mes. Sin la cota inferior, una licencia vencida
        // saldría también en «vence pronto» y se contaría dos veces.
        return $fin->lessThanOrEqualTo(now()->addMonth()) ? 'pronto' : 'activa';
    }

    public function isExpired(): bool
    {
        return $this->end_at && now()->greaterThan($this->end_at);
    }

    public function isValidNow(): bool
    {
        if (!$this->active) {
            return false;
        }

        if ($this->start_at && now()->lessThan($this->start_at)) {
            return false;
        }

        if ($this->end_at && now()->greaterThan($this->end_at)) {
            return false;
        }

        return true;
    }

    /**
     * Determina el estado de la licencia.
     * 
     * @return string 'activo'|'vencen_pronto'|'vencida'|'desactivada'
     */
    public function getStatus(): string
    {
        // Si la licencia no está activa
        if (!$this->active) {
            return 'desactivada';
        }

        // Si no tiene fecha de vencimiento, se considera activa
        if (!$this->end_at) {
            return 'activo';
        }

        $now = Carbon::now();
        $oneMonthFromNow = $now->copy()->addMonth();
        $endAt = Carbon::parse($this->end_at);

        // Verificar si está vencida (hoy o antes)
        if ($endAt->lte($now)) {
            return 'vencida';
        }

        // Verificar si vence pronto (dentro de un mes o menos, pero en el futuro)
        if ($endAt->lte($oneMonthFromNow)) {
            return 'vencen_pronto';
        }

        return 'activo';
    }

    /**
     * Un token de licencia nuevo: `3IP-XXXX-XXXX-XXXX-XXXX`.
     *
     * **El formato es contrato público con el plugin**: es la cadena que el cliente pega en
     * su Moodle, y la que viaja en cada petición a la API. Estaba definido en cuatro
     * pantallas a la vez —`Create`, `CreateWizard`, `Edit` y `LicenseForm`—, así que
     * cambiarlo tenía cuatro oportunidades de quedarse a medias y el fallo habría salido
     * solo en los tokens creados desde una de ellas (MGR-022). Ahora vive aquí, junto a la
     * tabla que consulta para comprobar que no se repite.
     *
     * **`random_int()` y no `rand()`, y esto no se toca.** Es el generador criptográfico de
     * PHP; con `rand()` o `mt_rand()` el token sería **adivinable a partir de otro**, y un
     * token es la credencial con la que un Moodle pide su licencia. Queda dicho aquí para
     * que nadie lo «optimice» en un refactor: no es un número aleatorio cualquiera.
     *
     * @param  int|null  $exceptoId  Fila que no cuenta para la unicidad. Lo usa la pantalla
     *                               de edición al regenerar el token de una licencia que ya
     *                               existe: su propio token no es un choque.
     */
    public static function generarToken(?int $exceptoId = null): string
    {
        // Sin vocales minúsculas ni caracteres que se confundan al dictarlo por teléfono:
        // el alfabeto es el de siempre, y también es contrato.
        $caracteres = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
        $total = strlen($caracteres);

        do {
            $bloques = [];

            for ($bloque = 0; $bloque < 4; $bloque++) {
                $letras = '';

                for ($letra = 0; $letra < 4; $letra++) {
                    $letras .= $caracteres[random_int(0, $total - 1)];
                }

                $bloques[] = $letras;
            }

            $token = '3IP-' . implode('-', $bloques);

            $yaExiste = static::query()
                ->where('token', $token)
                ->when($exceptoId !== null, fn ($q) => $q->where('id', '!=', $exceptoId))
                ->exists();
        } while ($yaExiste);

        return $token;
    }
}
