<?php

namespace App\Services\System;

use App\Mail\ApiBlockAlert;
use App\Models\Monitoring\ApiBlock;
use App\Models\Monitoring\Log as MonitoringLog;
use App\Models\Monitoring\Notice;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;

/**
 * Avisa por correo cuando algo pasa a bloqueado, **una sola vez por bloqueo**.
 *
 * Lo pedido fue "es importante mandar un email cuando hay un bloqueo SIEMPRE". Lo que
 * hace falta para que ese "siempre" sea útil y no un buzón inservible:
 *
 * - **Siempre activo**: el interruptor está en la pantalla de ajustes y viene
 *   encendido. Antes el aviso existía pero venía apagado (`API_ALERTS_ENABLED=false`),
 *   así que en la práctica no llegaba nunca.
 * - **Uno por bloqueo, no uno por rechazo**: el correo sale en la transición a
 *   bloqueado. El caso real de producción fueron 437 rechazos en 17 minutos desde una
 *   IP; con un correo por rechazo, el aviso se convierte en algo que se filtra y deja
 *   de leerse. Mientras el bloqueo siga abierto no se repite; si alguien lo libera y
 *   vuelve a pasar, se avisa otra vez.
 * - **`notified_at` en la fila**: si el envío falla, el bloqueo queda pendiente de
 *   avisar y el siguiente intento lo reintenta. No se pierde el aviso por un fallo del
 *   SMTP.
 *
 * El resumen diario va aparte (`ApiBlocksDigest`).
 */
class BlockNotifier
{
    public function __construct(private Settings $ajustes)
    {
    }

    /**
     * Deja el aviso en `notices`, que es lo que enseña la pantalla de Avisos.
     *
     * **Se registra también cuando falla**, con su error y sin fecha de envío: «los fallidos
     * primero» es la mitad del sentido de esa pantalla, y un SMTP caído no se vería en
     * ninguna parte hasta que alguien echara de menos un correo.
     *
     * La `reference` es la clave de idempotencia de `Notice::registrar()`, así que lleva el
     * id del episodio: un bloqueo avisa **una vez**, y si alguien lo libera y vuelve a
     * pasar, el episodio nuevo tiene otro id y vuelve a avisar. Es la misma regla que
     * `notified_at`, dicha en la tabla que se mira.
     *
     * Nunca lanza: lo llama `avisar()`, que no puede tumbar la petición de la API.
     *
     * @param  list<string>  $destinatarios
     */
    private function registrarElAviso(ApiBlock $bloqueo, array $destinatarios, ?string $error = null): void
    {
        try {
            Notice::registrar(
                Notice::SUBJECT_API_BLOCK,
                $bloqueo->id,
                Notice::KIND_BLOCK_ALERT,
                (int) $bloqueo->limit_value,
                'block:' . $bloqueo->id,
                $destinatarios,
                $error,
                [
                    'sujeto' => $bloqueo->sujeto_texto,
                    'motivo' => $bloqueo->reason,
                    // Si cortaba o solo observaba, que es lo que decide si alguien se quedó
                    // sin servicio — y es lo primero que se pregunta al leer el aviso.
                    'corta' => (bool) $bloqueo->enforced,
                    'rechazos' => (int) $bloqueo->hits,
                ]
            );
        } catch (\Throwable $e) {
            Log::channel('api')->error('No se pudo registrar el aviso de bloqueo', [
                'api_block_id' => $bloqueo->id,
                'error' => $e->getMessage(),
            ]);
        }
    }

    /**
     * Manda el aviso de un bloqueo nuevo si toca.
     *
     * Nunca lanza: un fallo al avisar no puede tumbar la petición de la API que ha
     * provocado el bloqueo.
     */
    public function avisar(ApiBlock $bloqueo): void
    {
        try {
            if (!$this->ajustes->avisaDeBloqueos()) {
                return;
            }

            if ($bloqueo->notified_at !== null) {
                return;
            }

            $destinatarios = $this->ajustes->destinatariosDeAvisos();

            if ($destinatarios === []) {
                // Sin destinatarios no hay aviso posible, y eso es un problema de
                // configuración que hay que poder ver: queda en el log del panel para
                // que salga en el Dashboard.
                MonitoringLog::db(
                    'warning',
                    '16007',
                    'Hay un bloqueo nuevo (' . $bloqueo->sujeto_texto . ') y no hay ningún '
                    . 'destinatario configurado para los avisos. Se configura en '
                    . 'Configuraciones → Configuraciones del Manager.',
                    'ApiBlock',
                    (string) $bloqueo->id
                );

                return;
            }

            Mail::to($destinatarios)->send(new ApiBlockAlert($bloqueo));

            $bloqueo->update(['notified_at' => now()]);

            MonitoringLog::db(
                'info',
                '16008',
                'Aviso de bloqueo enviado a ' . implode(', ', $destinatarios) . ': ' . $bloqueo->sujeto_texto,
                'ApiBlock',
                (string) $bloqueo->id
            );

            $this->registrarElAviso($bloqueo, $destinatarios);
        } catch (\Throwable $e) {
            // **El fallo también se registra**, y es la mitad del sentido de la pantalla de
            // Avisos: «los fallidos primero, un aviso que no salió es un cliente sin
            // avisar». Sin esta fila, un SMTP caído no se vería en ninguna parte hasta que
            // alguien echara de menos un correo.
            $this->registrarElAviso($bloqueo, $destinatarios ?? [], $e->getMessage());

            // `notified_at` se queda nulo a propósito: el bloqueo sigue pendiente de
            // avisar y el siguiente rechazo lo reintentará.
            Log::channel('api')->error('No se pudo enviar el aviso de bloqueo', [
                'api_block_id' => $bloqueo->id,
                'sujeto' => $bloqueo->sujeto_texto,
                'error' => $e->getMessage(),
            ]);
        }
    }
}
