<?php

namespace App\Http\Middleware;

use App\Models\ApiRequestLog;
use App\Services\Api\RequestLogSanitizer;
use App\Services\Api\Severidad;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;

class ApiRequestLogger
{
    public function handle(Request $request, Closure $next): Response
    {
        if (!$request->isMethod('POST') || !$request->is('api/v1') && !$request->is('api/v1/*')) {
            return $next($request);
        }

        $requestUuid = (string) \Illuminate\Support\Str::uuid();
        $startedAt = now();
        $hrtimeStart = hrtime(true);

        // Se expone en el request para que el resto del camino pueda usarlo como
        // referencia de correlación: cuando la API deja de devolver el detalle de un
        // error, responde este identificador y soporte lo busca en /api-logs.
        // Ver known-issues MGR-007.
        $request->attributes->set('api_request_uuid', $requestUuid);

        $response = null;
        try {
            $response = $next($request);
        } finally {
            if ($response !== null) {
            try {
                $endedAt = now();
                $durationNs = hrtime(true) - $hrtimeStart;
                $durationMs = (int) round($durationNs / 1e6);

                $httpStatus = $response->getStatusCode();

                $responseSize = null;
                if (!$response instanceof \Symfony\Component\HttpFoundation\StreamedResponse) {
                    try {
                        $content = $response->getContent();
                        $responseSize = $content !== false ? strlen($content) : null;
                    } catch (\Throwable) {
                        $responseSize = null;
                    }
                }

                $success = null;
                $errorCode = null;
                $errorMessage = null;
                $responseBody = null;
                try {
                    $content = $response->getContent();
                    if ($content !== false) {
                        $decoded = json_decode($content, true);
                        if (is_array($decoded)) {
                            $success = $decoded['success'] ?? null;
                            $errorCode = isset($decoded['code']) ? (int) $decoded['code'] : null;
                            $errorMessage = isset($decoded['error']) ? (string) $decoded['error'] : null;
                        }

                        $responseBody = self::cuerpoAGuardar($content, $httpStatus, $decoded);
                    }
                } catch (\Throwable) {
                    // leave success, errorCode, errorMessage as null
                }

                $bodyContent = $request->getContent();
                $bodyArray = null;
                if ($bodyContent !== false && $bodyContent !== '') {
                    $bodyArray = json_decode($bodyContent, true);
                }
                if (!is_array($bodyArray)) {
                    $bodyArray = [];
                }

                $action = $bodyArray['action'] ?? null;
                $plugin = $bodyArray['plugin'] ?? null;
                // Qué plugin ORIGINA la llamada, cuando no es el mismo que la firma:
                // el cliente lo envía y hasta ahora se tiraba (MGR-036).
                $callerplugin = $bodyArray['callerplugin'] ?? null;
                $host = $bodyArray['host'] ?? null;
                $version = $bodyArray['version'] ?? null;
                $projectid = $bodyArray['projectid'] ?? null;
                $environmentPayload = $bodyArray['environment'] ?? $bodyArray['site'] ?? null;
                if ($environmentPayload !== null && !is_array($environmentPayload)) {
                    $environmentPayload = null;
                }
                if (is_array($environmentPayload)) {
                    $environmentPayload = RequestLogSanitizer::redactSubtree($environmentPayload);
                }

                $payloadStored = RequestLogSanitizer::payloadForStorage($bodyArray);

                $allHeaders = $request->headers->all();
                $sanitizedHeaders = RequestLogSanitizer::sanitizeHeaders($allHeaders);

                $queryParams = $request->query->all();

                $bearerToken = $request->bearerToken();
                $authType = $bearerToken ? 'bearer' : 'none';
                $bearerTokenHash = null;
                if ($bearerToken && $httpStatus >= 400) {
                    $bearerTokenHash = hash('sha256', $bearerToken);
                }

                $licenseTokenId = null;
                $environmentId = null;
                $clientId = null;
                $licenseToken = $request->get('_license_token');
                $environment = $request->get('_environment');
                if ($licenseToken instanceof \App\Models\Products\LicenseToken) {
                    $licenseTokenId = $licenseToken->id;
                    $clientId = $licenseToken->client_id;
                }
                if ($environment instanceof \App\Models\Environments\Environment) {
                    $environmentId = $environment->id;
                    if ($clientId === null && $environment->client_id) {
                        $clientId = $environment->client_id;
                    }
                }

                $fila = ApiRequestLog::create([
                    'request_uuid' => $requestUuid,
                    'method' => $request->getMethod(),
                    'path' => $request->path(),
                    'ip' => $request->ip(),
                    'user_agent' => $request->userAgent(),
                    'headers' => $sanitizedHeaders,
                    'query' => $queryParams ?: null,
                    'auth_type' => $authType,
                    'bearer_token_hash' => $bearerTokenHash,
                    'license_token_id' => $licenseTokenId,
                    'environment_id' => $environmentId,
                    'client_id' => $clientId,
                    'action' => $action ? (string) $action : null,
                    'plugin' => $plugin ? (string) $plugin : null,
                    'callerplugin' => is_string($callerplugin) && $callerplugin !== '' ? $callerplugin : null,
                    'host' => $host ? (string) $host : null,
                    'version' => $version ? (string) $version : null,
                    'projectid' => $projectid ? (string) $projectid : null,
                    'environment' => $environmentPayload,
                    'payload' => $payloadStored,
                    'http_status' => $httpStatus,
                    'success' => $success,
                    'error_code' => $errorCode,
                    'error' => $errorMessage,
                    // `severity` no se pasa: la rellena el propio modelo al crear la
                    // fila. Una sola fuente de verdad, y así una fila creada desde un
                    // test o un comando también queda bien clasificada.
                    'response_size' => $responseSize,
                    'response_body' => $responseBody,
                    'duration_ms' => $durationMs,
                    'started_at' => $startedAt,
                    'ended_at' => $endedAt,
                ]);

                self::dejarRastroDelError($fila);
            } catch (\Throwable $e) {
                // Que falle el logger es un fallo de la aplicación y va al log de
                // siempre. Pero **con lo esencial de la petición dentro**: si esto se
                // limita a decir «no pude guardar», el error de la API que se estaba
                // registrando desaparece sin dejar nada (MGR-003).
                Log::error('ApiRequestLogger failed to persist log', [
                    'request_uuid' => $requestUuid ?? 'unknown',
                    'exception' => $e->getMessage(),
                    'http_status' => $httpStatus ?? null,
                    'host' => isset($host) && is_scalar($host) ? (string) $host : null,
                    'action' => isset($action) && is_scalar($action) ? (string) $action : null,
                    'error_code' => $errorCode ?? null,
                    'error' => $errorMessage ?? null,
                ]);
            }
            }
        }

        return $response;
    }

    /**
     * Deja una línea en el log del servidor cuando la petición ha sido un **error**.
     *
     * **El problema que resuelve** (MGR-003): un `401 Token no válido` vivía solo en
     * `api_request_logs`, así que diagnosticar una integración exigía entrar al panel.
     * Cuatro rechazos seguidos de un Moodle y `tail -f` no enseñaba **ni una línea**.
     *
     * **Y solo `error`, esto es lo importante.** Las otras tres severidades son
     * respuestas correctas: `sin_contenido` es un sitio al que no le toca ese contenido,
     * `negocio` es una licencia que no cubre lo que pide, y los códigos `*002` no son
     * averías —ver `norma-cero-errores.md`—. Si esto escribiera las cuatro, el fichero se
     * llenaría de tráfico normal, nadie lo miraría, y habríamos cambiado «no hay rastro»
     * por «el rastro no se puede leer», que es peor porque parece resuelto.
     *
     * Va al canal `api` y no a `laravel.log` **a propósito**, y esto corrige lo que
     * proponía la ficha del issue: la decisión ya estaba tomada en `config/logging.php`
     * —«el tráfico de la API es de otro orden de magnitud y ahogaría el log de la
     * aplicación»—. El `tail -f` es sobre `storage/logs/api.log`.
     *
     * Una línea por error, con lo justo para saber a quién llamar sin abrir el panel; el
     * detalle completo sigue en la fila, y `request_uuid` es lo que las une: es el mismo
     * identificador que la API le devuelve al plugin (MGR-007).
     */
    private static function dejarRastroDelError(ApiRequestLog $fila): void
    {
        if ($fila->severity !== Severidad::ERROR) {
            return;
        }

        try {
            Log::channel('api')->error('Petición de la API con error', [
                'request_uuid' => $fila->request_uuid,
                'host' => $fila->host,
                'action' => $fila->action,
                'plugin' => $fila->plugin,
                'http_status' => $fila->http_status,
                'code' => $fila->error_code,
                'error' => $fila->error,
                'duration_ms' => $fila->duration_ms,
            ]);
        } catch (\Throwable) {
            // Escribir en el log no puede tumbar una respuesta de la API: la fila ya
            // está guardada, que es lo que no se puede perder.
        }
    }

    /** Tope del cuerpo guardado. Un error de la API cabe de sobra; una traza de PHP, no. */
    private const CUERPO_MAX_CARACTERES = 2000;

    /**
     * El cuerpo de la respuesta que se guarda… **y solo cuando algo ha ido mal**.
     *
     * `api_request_logs` es la tabla que más crece del sistema y las respuestas correctas
     * son justo las grandes —`scss`, `js`, `setup` y `scss-cdn` devuelven ficheros—, así
     * que guardarlas todas multiplicaría la tabla por el peso del contenido servido para
     * un dato que casi nunca se mira: si la respuesta fue bien, `response_size` ya lo dice
     * todo.
     *
     * `http_status >= 400` es **exactamente** la definición de «no correcta» de
     * {@see \App\Services\Api\Severidad::de()}: ahí todo lo que baja de 400 devuelve `ok`
     * antes de mirar nada más. Se comprueba por el status y no por la severidad porque la
     * severidad la calcula el modelo al insertar, o sea después de este punto.
     *
     * **Se sanea si es JSON** con el mismo redactor que los `headers` y el `payload`: son
     * errores de nuestra propia API y no llevan credenciales, pero el redactor es barato y
     * la garantía vale más que el ahorro. Si no es JSON —una excepción que devuelve HTML,
     * un 502 de un proxy— se guarda el texto tal cual, **que es justo el caso en el que
     * hoy no quedaba ningún rastro de lo que pasó**.
     *
     * @param  array<mixed>|null  $decoded  El cuerpo ya decodificado, si era JSON.
     */
    private static function cuerpoAGuardar(string $content, int $httpStatus, ?array $decoded): ?string
    {
        if ($httpStatus < 400 || $content === '') {
            return null;
        }

        if (is_array($decoded)) {
            $saneado = json_encode(
                RequestLogSanitizer::redactSubtree($decoded),
                JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
            );

            $content = $saneado !== false ? $saneado : $content;
        }

        // Por caracteres y no por bytes: cortar a mitad de un UTF-8 deja la columna con
        // basura que después no se puede ni pintar.
        if (mb_strlen($content) <= self::CUERPO_MAX_CARACTERES) {
            return $content;
        }

        return mb_substr($content, 0, self::CUERPO_MAX_CARACTERES) . "\n…[recortado]";
    }
}
