<?php

namespace App\Support;

use App\Models\ApiRequestLog;
use App\Models\Products\Product;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;

/**
 * Quién ha pedido de verdad este contenido, y qué se le respondió.
 *
 * **Es distinto de `RepartoDeVersiones` y las dos cosas hacen falta.** Ese calcula quién
 * *debería* recibir cada versión a partir del inventario de plugins; esto lee el registro
 * de la API y dice **quién llamó, cuándo, con qué versión y si le fue bien**. Un cliente
 * puede tener el plugin instalado —así que «debería recibir»— y no haber llamado nunca,
 * porque su tarea programada no corre o el sitio no sale a internet: eso solo se ve aquí.
 *
 * Y al revés, el caso que motivó esto: en la base de desarrollo, `theme_fresk` pidió
 * `scss-cdn` y recibió un **3002** —«no hay versión compatible»— porque no hay ni un
 * bundle publicado. Un cliente pidiendo algo que no existe, y desde el panel no se veía en
 * ninguna pantalla.
 *
 * El registro guarda la versión que **envía el cliente** (su `versiondb`), no la de
 * contenido que se le sirvió: por eso esto se agrupa por entorno y no por versión de
 * contenido.
 */
class PeticionesDeContenido
{
    /**
     * Cuántos días atrás se mira.
     *
     * `api_request_logs` es la tabla que más crece del sistema —una fila por llamada de
     * cada Moodle del parque—, así que la consulta va acotada por fecha para poder usar el
     * índice de `started_at`. Treinta días es suficiente: el contenido se pide en cada
     * cron del cliente, así que un sitio activo aparece varias veces al día.
     */
    private const DIAS = 30;

    /**
     * Las peticiones de una acción de contenido para un producto, agrupadas por entorno.
     *
     * Se filtra por `plugin`, que es el componente que el cliente declara en la petición
     * —el mismo identificador que el `slug` del producto—, y no solo por `action`: la
     * acción `features` la piden todos los productos, y sin el plugin se mezclarían las
     * peticiones de unos con las de otros.
     *
     * @return array{
     *     porEntorno: Collection<int, array<string, mixed>>,
     *     total: int,
     *     conError: int,
     *     desde: Carbon
     * }
     */
    public static function de(Product $producto, string $accion): array
    {
        $desde = now()->subDays(self::DIAS);

        // Con tope de tiempo: esta tabla puede ser enorme y la pantalla tiene que pintarse
        // igual. Si no llega a tiempo, se devuelve el hueco y quien llama lo dice.
        $filas = ConsultaAcotada::ejecutar(
            fn () => ApiRequestLog::query()
                ->where('action', $accion)
                ->where('plugin', $producto->slug)
                ->where('started_at', '>=', $desde)
                ->with('site:id,name,domain,env')
                ->orderByDesc('started_at')
                ->get([
                    'id', 'environment_id', 'client_id', 'host', 'version',
                    'http_status', 'error_code', 'started_at',
                ]),
            donde: 'contenido: peticiones de ' . $accion
        );

        if ($filas === null) {
            return [
                'porEntorno' => collect(),
                'total' => 0,
                'conError' => 0,
                'desde' => $desde,
                'agotado' => true,
            ];
        }

        $porEntorno = $filas
            // Por entorno cuando se sabe cuál es, y por dominio cuando no: una petición con
            // un token que no resuelve a ningún entorno **también es información** —alguien
            // está llamando— y agruparla como «null» la haría desaparecer.
            ->groupBy(fn (ApiRequestLog $log) => $log->environment_id ?? 'host:' . $log->host)
            ->map(function (Collection $suyas) {
                $ultima = $suyas->first();
                $fallidas = $suyas->filter(fn (ApiRequestLog $l) => $l->http_status >= 400);

                return [
                    'entorno' => $ultima->site,
                    'host' => $ultima->host,
                    'peticiones' => $suyas->count(),
                    'fallidas' => $fallidas->count(),
                    // La versión que envía el plugin. Si un sitio se actualizó por el
                    // medio, aparecen varias: se enseñan todas, porque ver dos versiones
                    // del mismo sitio es la señal de que se actualizó.
                    'versiones' => $suyas->pluck('version')->filter()->unique()->values(),
                    'ultima' => $ultima->started_at,
                    // El código del último fallo, que es el que hay que arreglar. `3002`
                    // significa «no hay versión compatible»: pidió y no había nada para él.
                    //
                    // **Como cadena.** La columna es `int` y sin cast en el modelo, así que
                    // llega como entero; la familia del código se compara como texto en
                    // todo el proyecto —`3002`, `4002`, `5002`— y un `=== '3002'` contra un
                    // entero es siempre falso, sin que nada falle: el mensaje que explica
                    // el error simplemente no saldría.
                    'ultimoError' => ($codigo = $fallidas->first()?->error_code) !== null
                        ? (string) $codigo
                        : null,
                    // **La distinción que importa**: todas fallaron, o solo algunas. Si
                    // todas fallan, ese cliente no ha recibido este contenido nunca.
                    'siempreFalla' => $fallidas->count() === $suyas->count(),
                ];
            })
            ->sortByDesc('ultima')
            ->values();

        return [
            'porEntorno' => $porEntorno,
            'total' => $filas->count(),
            'conError' => $filas->filter(fn (ApiRequestLog $l) => $l->http_status >= 400)->count(),
            'desde' => $desde,
            'agotado' => false,
        ];
    }
}
