<?php

namespace App\Console\Commands;

use App\Mail\SupportExpiryAlert;
use App\Models\Monitoring\Notice;
use App\Models\Support\EnvironmentSupport;
use App\Services\System\Settings;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;

/**
 * Soportes que caducan: avisa a los umbrales configurados.
 *
 * Recupera el `support:expires` del Manager antiguo, que era el aviso número dos de los
 * que se perdieron en la migración (ver `manager-anterior.md`). Avisaba a Soporte a
 * **30, 15, 7 y 0 días** de la caducidad, con los umbrales y el destinatario
 * configurables desde el panel. Aquí es igual.
 *
 * **Qué NO hace: tocar datos.** Al contrario que `licences:check-expiry`, que desactiva
 * la licencia vencida, aquí no se desactiva nada. Un soporte es un **acuerdo
 * comercial**: caducar no corta el servicio —eso lo decide la licencia— y "desactivar"
 * un contrato terminado no significa nada. Lo único que hace falta es que alguien lo
 * sepa a tiempo.
 *
 * **Varios umbrales, un aviso por umbral.** La clave está en `notices`, con
 * `threshold` = el umbral y `reference` = la fecha de fin. Así:
 *
 *   - cada umbral avisa **una sola vez** (nada de un correo diario durante 30 días);
 *   - **renovar rearma los avisos por construcción**, porque al cambiar la fecha de fin
 *     cambia la referencia y ningún aviso viejo casa. Sin `reset()` que alguien pueda
 *     olvidarse de llamar, que es como estaba en el Manager antiguo.
 *
 * **Se avisa del umbral más cercano que aún no se haya avisado.** Si la tarea ha estado
 * parada una semana y un soporte se ha saltado el umbral de 15, al volver se avisa del
 * de 15 —el que corresponde— y no se manda un correo por cada umbral perdido.
 */
class CheckSupportExpiry extends Command
{
    protected $signature = 'supports:check-expiry
                            {--dry-run : Muestra lo que haría, sin enviar nada}';

    protected $description = 'Avisa de los soportes de entorno que están por caducar o ya han caducado';

    public function handle(Settings $ajustes): int
    {
        $seco = (bool) $this->option('dry-run');

        if (!$ajustes->avisaDeSoportes()) {
            $this->warn('Los avisos de soportes están desactivados en la configuración.');

            return self::SUCCESS;
        }

        $destinatarios = $ajustes->destinatariosDeSoportes();

        if ($destinatarios === [] && !$seco) {
            // Aquí sí se aborta, al contrario que en las licencias: allí había que
            // desactivar igual, y esta tarea no hace nada más que avisar.
            $this->warn('No hay destinatarios para los avisos de soportes: no se envía nada.');

            return self::SUCCESS;
        }

        $umbrales = $ajustes->diasDeAvisoDeSoportes();
        $this->line('Umbrales de aviso: ' . implode(', ', $umbrales) . ' día(s).');

        $enviados = 0;

        foreach ($this->contratosVivos() as $contrato) {
            $restantes = $contrato->diasRestantes();
            $umbral = $this->umbralQueToca($umbrales, $restantes);

            if ($umbral === null) {
                continue;
            }

            $referencia = $contrato->end_at->format('Y-m-d');

            if (Notice::yaEnviado(
                Notice::SUBJECT_ENVIRONMENT_SUPPORT,
                $contrato->id,
                Notice::KIND_SUPPORT_EXPIRING,
                $umbral,
                $referencia
            )) {
                continue;
            }

            $entorno = $contrato->environment?->name ?? 'entorno ' . $contrato->environment_id;

            $this->line($restantes < 1
                ? "  CADUCADO hace " . abs($restantes) . " día(s): {$entorno}"
                : "  Por caducar en {$restantes} día(s) [umbral {$umbral}]: {$entorno}");

            if ($seco) {
                $enviados++;

                continue;
            }

            $this->enviar($contrato, $umbral, $referencia, $destinatarios, $restantes);
            $enviados++;
        }

        $this->info(($seco ? '[simulación] ' : '') . "Avisos de soporte: {$enviados}");

        return self::SUCCESS;
    }

    /**
     * Contratos de los que tiene sentido avisar.
     *
     * **No se usa `environments.environment_support_id`**, que es el puntero al contrato
     * vigente: en la base real estaba a NULL en todos los entornos, así que un aviso que
     * dependiera de él no avisaría de nada y en silencio. Se recorre la tabla de
     * contratos, que es el dato de verdad.
     *
     * El corte inferior evita resucitar historia: sin él, la primera ejecución mandaría
     * un correo por cada soporte terminado en 2021.
     *
     * @return \Illuminate\Support\Collection<int, EnvironmentSupport>
     */
    private function contratosVivos()
    {
        return EnvironmentSupport::with(['environment.client', 'support'])
            // Solo de clientes de alta: avisar del soporte de un cliente que se fue es
            // ruido que hace que se dejen de leer los avisos que sí importan.
            ->whereHas('environment.client', fn ($q) => $q->where('actived', true))
            ->whereDate('end_at', '>=', now()->subDays(30)->format('Y-m-d'))
            ->orderBy('end_at')
            ->get();
    }

    /**
     * El umbral que corresponde a los días que quedan.
     *
     * Es el **más pequeño de los umbrales que ya se han alcanzado**: con 30/15/7/0 y 12
     * días restantes toca el de 15, no el de 30 —ese ya se avisó— ni el de 7, que aún no
     * llega. Un soporte ya caducado va siempre al umbral 0.
     *
     * @param  array<int, int>  $umbrales  De mayor a menor
     */
    private function umbralQueToca(array $umbrales, int $restantes): ?int
    {
        if ($restantes < 1) {
            return in_array(0, $umbrales, true) ? 0 : null;
        }

        $alcanzados = array_filter($umbrales, fn (int $umbral) => $restantes <= $umbral);

        return $alcanzados === [] ? null : min($alcanzados);
    }

    /**
     * Envía el aviso y lo registra. Un fallo de correo no aborta el recorrido: los demás
     * soportes tienen que avisarse igual.
     *
     * @param  array<int, string>  $destinatarios
     */
    private function enviar(
        EnvironmentSupport $contrato,
        int $umbral,
        string $referencia,
        array $destinatarios,
        int $dias
    ): void {
        try {
            Mail::to($destinatarios)->send(new SupportExpiryAlert($contrato, $dias));

            Notice::registrar(
                Notice::SUBJECT_ENVIRONMENT_SUPPORT,
                $contrato->id,
                Notice::KIND_SUPPORT_EXPIRING,
                $umbral,
                $referencia,
                $destinatarios,
                null,
                [
                    'dias' => $dias,
                    'entorno' => $contrato->environment?->name,
                    'cliente' => $contrato->environment?->client?->name,
                    'soporte' => $contrato->support?->name,
                ]
            );
        } catch (\Throwable $e) {
            // Sin `sent_at`: la próxima pasada lo reintenta.
            Notice::registrar(
                Notice::SUBJECT_ENVIRONMENT_SUPPORT,
                $contrato->id,
                Notice::KIND_SUPPORT_EXPIRING,
                $umbral,
                $referencia,
                $destinatarios,
                $e->getMessage()
            );

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