<?php

namespace App\Console\Commands;

use App\Models\ApiRequestLog;
use App\Services\System\ApiLogPurger;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;

/**
 * Borrado de registros de `api_request_logs` desde consola.
 *
 * Existe por una razón concreta: cuando la tabla crece, el visor `/api-logs` se
 * vuelve inusable (ver known-issues MGR-027), y un botón dentro de una página que
 * no se puede abrir no sirve de nada. Este comando funciona con la web caída, con
 * la tabla a millones de filas y sin cargar nada en memoria.
 *
 * Ejemplos:
 *   php artisan api-logs:purge --days=90            borra lo anterior a 90 días
 *   php artisan api-logs:purge --days=90 --dry-run  solo dice cuántos serían
 *   php artisan api-logs:purge --days=7 --only-errors
 *   php artisan api-logs:purge --all                vacía la tabla (pide confirmación)
 */
class PurgeApiLogs extends Command
{
    protected $signature = 'api-logs:purge
        {--days= : Borra los registros anteriores a N días}
        {--retention : Usa los días configurados en el panel; si no hay retención, no hace nada}
        {--all : Borra todos los registros}
        {--only-errors : Solo los que fallaron (success = false)}
        {--status= : Solo un código HTTP concreto (p. ej. 401)}
        {--ip= : Solo los de una IP}
        {--dry-run : No borra nada; muestra qué se borraría}
        {--chunk=2000 : Filas por lote}';

    protected $description = 'Borra registros del log de peticiones de la API (api_request_logs)';

    public function handle(): int
    {
        $days = $this->option('days');
        $all = (bool) $this->option('all');

        // `--retention` es el modo de la tarea programada: los días los decide el panel,
        // no el crontab. Así cambiar la retención tiene efecto sin tocar el servidor, y
        // mientras nadie la configure la tarea corre todos los días sin borrar nada.
        if ($this->option('retention')) {
            if ($days !== null || $all) {
                $this->error('--retention no se combina con --days ni --all.');

                return self::FAILURE;
            }

            $retencion = app(\App\Services\System\Settings::class)->diasDeRetencionDeLogs();

            if ($retencion === 0) {
                $this->info('No hay retención configurada: no se borra nada.');

                return self::SUCCESS;
            }

            $days = (string) $retencion;
            $this->line('Retención configurada: <fg=yellow>' . $retencion . '</> días.');
        }

        if (!$all && $days === null) {
            $this->error('Indica --days=N o --all. Sin uno de los dos no se borra nada.');

            return self::FAILURE;
        }

        if ($all && $days !== null) {
            $this->error('--all y --days son incompatibles: elige uno.');

            return self::FAILURE;
        }

        $seleccion = fn () => $this->query($days, $all);

        $total = $seleccion()->count();

        if ($total === 0) {
            $this->info('No hay registros que coincidan. No se borra nada.');

            return self::SUCCESS;
        }

        $this->newLine();
        $this->line('Registros seleccionados: <fg=yellow>' . number_format($total, 0, ',', '.') . '</>');
        $this->desglose($seleccion());

        if ($this->option('dry-run')) {
            $this->newLine();
            $this->info('--dry-run: no se ha borrado nada.');

            return self::SUCCESS;
        }

        // `--all` vacía el rastro de auditoría entero: eso se pregunta siempre,
        // incluso con --no-interaction detrás (ahí `confirm` devuelve el default).
        if ($all && !$this->confirm('Vas a borrar TODOS los registros del log. ¿Seguro?', false)) {
            $this->info('Cancelado.');

            return self::SUCCESS;
        }

        $barra = $this->output->createProgressBar($total);
        $barra->start();

        $criterio = $all
            ? 'todos'
            : 'anteriores a ' . (int) $days . ' días'
                . ($this->option('retention') ? ', por retención automática' : '');

        // El borrado por lotes y el rastro son los mismos que usa el panel: ver
        // `ApiLogPurger`. Tres copias de esto acabarían no estando de acuerdo.
        $borrados = app(ApiLogPurger::class)->borrar(
            $seleccion,
            $criterio,
            'consola',
            (int) $this->option('chunk'),
            fn (int $lote) => $barra->advance($lote)
        );

        $barra->finish();
        $this->newLine(2);

        $this->info(number_format($borrados, 0, ',', '.') . ' registro(s) borrados.');

        return self::SUCCESS;
    }

    /**
     * Selección a borrar. Se reconstruye en cada lote a propósito: reutilizar el
     * mismo builder con `limit()` en un bucle arrastra estado.
     */
    private function query(?string $days, bool $all)
    {
        $q = ApiRequestLog::query();

        if (!$all) {
            $q->where('started_at', '<', now()->subDays((int) $days));
        }

        if ($this->option('only-errors')) {
            $q->where('success', false);
        }

        if ($this->option('status')) {
            $q->where('http_status', (int) $this->option('status'));
        }

        if ($this->option('ip')) {
            $q->where('ip', $this->option('ip'));
        }

        return $q;
    }

    /**
     * Desglose de lo que se va a borrar. Además de dar seguridad antes de un
     * borrado, es la vía rápida para ver QUÉ está saturando la tabla.
     */
    private function desglose($query): void
    {
        $porEstado = (clone $query)
            ->select('http_status', DB::raw('count(*) as c'))
            ->groupBy('http_status')
            ->orderByDesc('c')
            ->limit(5)
            ->pluck('c', 'http_status');

        $porAccion = (clone $query)
            ->select('action', DB::raw('count(*) as c'))
            ->groupBy('action')
            ->orderByDesc('c')
            ->limit(5)
            ->pluck('c', 'action');

        if ($porEstado->isNotEmpty()) {
            $this->line('  por estado HTTP: ' . $porEstado->map(
                fn ($c, $estado) => ($estado ?: '?') . '=' . number_format($c, 0, ',', '.')
            )->implode('  '));
        }

        if ($porAccion->isNotEmpty()) {
            $this->line('  por acción:      ' . $porAccion->map(
                fn ($c, $accion) => ($accion ?: '?') . '=' . number_format($c, 0, ',', '.')
            )->implode('  '));
        }
    }
}
