<?php

namespace App\Console\Commands;

use App\Mail\LicenceExpiryAlert;
use App\Models\Monitoring\Log as MonitoringLog;
use App\Models\Monitoring\Notice;
use App\Models\Products\LicenseToken;
use App\Services\System\Settings;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;

/**
 * Licencias que caducan: avisa antes, y al pasarse las desactiva.
 *
 * **El problema que resuelve.** Cuando una licencia pasa su `end_at`, el middleware ya
 * corta —`Token expirado`, 403— así que los Moodles de ese cliente dejan de recibir
 * licencias y contenido. Pero en la base seguía marcada como **activa**, y eso es una
 * mentira en el panel: la ficha dice "activa" y la realidad dice "cortada". Y nadie
 * avisaba: el Dashboard lo muestra, pero solo si alguien entra a mirarlo.
 *
 * Comprobado en la base local al escribir esto: había una licencia **vencida hacía 89
 * días y todavía marcada como activa**.
 *
 * **Qué hace, en dos pasos:**
 *
 *   1. **Aviso previo** a N días de caducar (N configurable, 10 por defecto).
 *   2. **Al pasarse**: avisa y **desactiva** la licencia, para que el panel diga la
 *      verdad. No cambia nada para el cliente —ya estaba cortado por la fecha—, cambia
 *      lo que ve quien administra.
 *
 * **Por qué desactivar y no dejarla "caducada".** Una licencia vencida pero activa se
 * lee como un problema pendiente de mirar, y a los tres meses ya nadie la mira. Al
 * desactivarla queda en un estado explícito, coherente con lo que hace la API, y el
 * aviso deja constancia de por qué.
 *
 * **No pelea con quien la reactiva a mano.** El aviso y la desactivación se registran en
 * `notices` con la fecha de fin como referencia: si alguien reactiva la licencia sin
 * cambiar la fecha, esta tarea **no vuelve a desactivarla** —ya está registrado—. Si la
 * renueva cambiando la fecha, la referencia cambia y el ciclo empieza de cero.
 */
class CheckLicenceExpiry extends Command
{
    protected $signature = 'licences:check-expiry
                            {--dry-run : Muestra lo que haría, sin enviar ni desactivar}';

    protected $description = 'Avisa de las licencias que caducan y desactiva las que ya han caducado';

    public function handle(Settings $ajustes): int
    {
        $seco = (bool) $this->option('dry-run');
        $dias = $ajustes->diasDeAvisoDeLicencias();
        $destinatarios = $ajustes->destinatariosDeLicencias();
        $avisaPorCorreo = $ajustes->avisaDeLicencias() && $destinatarios !== [];

        if (!$avisaPorCorreo) {
            // Se dice, pero NO se aborta: desactivar las caducadas hay que hacerlo
            // igual, aunque no haya a quién avisar. La coherencia del panel no depende
            // de que el correo esté configurado.
            $this->warn($ajustes->avisaDeLicencias()
                ? 'No hay destinatarios para los avisos de licencias: solo se desactivarán las caducadas.'
                : 'Los avisos de licencias están desactivados: solo se desactivarán las caducadas.');
        }

        $avisadas = $this->avisarDeLasQueVanACaducar($dias, $destinatarios, $avisaPorCorreo, $seco);
        $desactivadas = $this->tratarLasCaducadas($destinatarios, $avisaPorCorreo, $seco);

        $this->info(($seco ? '[simulación] ' : '')
            . "Avisos previos: {$avisadas} · Caducadas tratadas: {$desactivadas}");

        return self::SUCCESS;
    }

    /**
     * Aviso previo: licencias activas cuya fecha de fin cae en los próximos N días.
     */
    private function avisarDeLasQueVanACaducar(int $dias, array $destinatarios, bool $avisa, bool $seco): int
    {
        // Rango CERRADO: sin la cota inferior entrarían también las ya vencidas, que
        // son el otro caso y llevan otro correo. Es el error de MGR-025.
        $licencias = LicenseToken::with('client')
            ->where('active', true)
            // Solo de clientes de alta: un cliente dado de baja no necesita que Comercial
            // reciba avisos de sus licencias, y los recibía.
            ->whereHas('client', fn ($q) => $q->where('actived', true))
            ->whereNotNull('end_at')
            ->whereBetween('end_at', [now(), now()->addDays($dias)])
            ->orderBy('end_at')
            ->get();

        $enviados = 0;

        foreach ($licencias as $licencia) {
            $referencia = $licencia->end_at->format('Y-m-d');

            if (Notice::yaEnviado(
                Notice::SUBJECT_LICENSE_TOKEN,
                $licencia->id,
                Notice::KIND_LICENCE_EXPIRING,
                $dias,
                $referencia
            )) {
                continue;
            }

            $restantes = (int) ceil(now()->diffInDays($licencia->end_at, false));

            $this->line("  Por caducar: {$licencia->name} ({$licencia->client?->name}) en {$restantes} día(s)");

            if ($seco) {
                $enviados++;

                continue;
            }

            if (!$avisa) {
                continue;
            }

            $this->enviar($licencia, Notice::KIND_LICENCE_EXPIRING, $dias, $referencia, $destinatarios, $restantes);
            $enviados++;
        }

        return $enviados;
    }

    /**
     * Las que ya han caducado y siguen activas: aviso y desactivación.
     */
    private function tratarLasCaducadas(array $destinatarios, bool $avisa, bool $seco): int
    {
        $licencias = LicenseToken::with('client')
            ->where('active', true)
            // Igual que arriba. Y además: la licencia de un cliente de baja YA está
            // desactivada por la propia baja, así que no habría nada que desactivar.
            ->whereHas('client', fn ($q) => $q->where('actived', true))
            ->whereNotNull('end_at')
            ->where('end_at', '<', now())
            ->orderBy('end_at')
            ->get();

        $tratadas = 0;

        foreach ($licencias as $licencia) {
            $referencia = $licencia->end_at->format('Y-m-d');

            // Si ya se trató esta caducidad, no se toca: respeta a quien la haya
            // reactivado a mano sin cambiar la fecha.
            if (Notice::yaEnviado(
                Notice::SUBJECT_LICENSE_TOKEN,
                $licencia->id,
                Notice::KIND_LICENCE_EXPIRED,
                0,
                $referencia
            )) {
                continue;
            }

            // El orden importa: en Carbon 3 `diffInDays` va CON signo, así que
            // `now()->diffInDays($pasado)` da negativo y salía "caducada hace -89
            // días". Se cuenta desde la fecha de fin hasta hoy. Es la misma trampa que
            // MGR-030.
            $vencidaHace = (int) $licencia->end_at->diffInDays(now());

            $this->line("  CADUCADA hace {$vencidaHace} día(s): {$licencia->name} ({$licencia->client?->name}) → se desactiva");

            if ($seco) {
                $tratadas++;

                continue;
            }

            // Primero el correo y después la desactivación: si el correo falla, el
            // `notices` queda sin `sent_at` y la próxima pasada lo reintenta. Al
            // revés, una licencia desactivada sin aviso enviado se quedaría muda.
            if ($avisa) {
                $this->enviar($licencia, Notice::KIND_LICENCE_EXPIRED, 0, $referencia, $destinatarios, -$vencidaHace);
            } else {
                // Sin correo, se registra igual para no repetir la desactivación.
                Notice::registrar(
                    Notice::SUBJECT_LICENSE_TOKEN,
                    $licencia->id,
                    Notice::KIND_LICENCE_EXPIRED,
                    0,
                    $referencia,
                    [],
                    null,
                    ['sin_destinatarios' => true]
                );
            }

            $licencia->update(['active' => false]);

            MonitoringLog::db(
                'warning',
                '16010',
                'Licencia desactivada automáticamente por caducidad: ' . $licencia->name
                . ' (' . ($licencia->client?->name ?? 'sin cliente') . '), venció el '
                . $licencia->end_at->format('d/m/Y'),
                'LicenseToken',
                (string) $licencia->id
            );

            $tratadas++;
        }

        return $tratadas;
    }

    /**
     * Envía el aviso y lo registra. Un fallo de correo no aborta el recorrido: las
     * demás licencias tienen que tratarse igual.
     */
    private function enviar(
        LicenseToken $licencia,
        string $kind,
        int $umbral,
        string $referencia,
        array $destinatarios,
        int $dias
    ): void {
        try {
            Mail::to($destinatarios)->send(new LicenceExpiryAlert($licencia, $dias));

            Notice::registrar(
                Notice::SUBJECT_LICENSE_TOKEN,
                $licencia->id,
                $kind,
                $umbral,
                $referencia,
                $destinatarios,
                null,
                ['dias' => $dias, 'cliente' => $licencia->client?->name]
            );
        } catch (\Throwable $e) {
            Notice::registrar(
                Notice::SUBJECT_LICENSE_TOKEN,
                $licencia->id,
                $kind,
                $umbral,
                $referencia,
                $destinatarios,
                $e->getMessage()
            );

            $this->error('    No se pudo enviar el aviso: ' . $e->getMessage());
        }
    }
}
