<?php

namespace WPD\Diagnostics\Checks;

use WPD\Diagnostics\CheckInterface;
use WPD\Domain\CheckResult;
use WPD\Domain\Finding;
use WPD\Domain\ScanContext;
use WPD\Security\EvidenceNormalizer;

final class ErrorLogCheck implements CheckInterface
{
    public function id()
    {
        return 'error_logs';
    }

    public function label()
    {
        return 'Registros de errores';
    }

    public function run(ScanContext $context, array $cursor)
    {
        $candidates = array(
            $context->root() . '/wp-content/debug.log',
            $context->root() . '/error_log',
            $context->root() . '/php_errorlog',
        );
        $groups = array();
        $normalizer = new EvidenceNormalizer();
        $read = 0;
        $partial = false;
        foreach ($candidates as $candidate) {
            $path = $context->guard()->safeFile($candidate);
            if ($path === null || !is_readable($path)) {
                continue;
            }
            $read++;
            list($contents, $wasPartial) = $this->tail($path, 524288);
            $partial = $partial || $wasPartial;
            if ($contents === '') {
                continue;
            }
            $lines = preg_split('/\R/', $contents);
            foreach ($lines as $line) {
                if (!preg_match('/(?:PHP\s+)?(?:Fatal error|Parse error)|Uncaught\s+(?:Error|Exception)|Allowed memory size|Call to undefined function|Failed opening required|Access denied for user/i', $line)) {
                    continue;
                }
                $type = $this->classify($line);
                $groups[$type] = $normalizer->logLine($line, $type, $context->root());
            }
        }

        $findings = array();
        foreach ($groups as $type => $evidence) {
            $findings[] = $this->finding($type, $evidence);
        }
        if ($read === 0) {
            return CheckResult::complete(array(), 'no disponible', 'No se encontraron registros locales legibles.');
        }
        if ($partial) {
            $findings[] = new Finding(
                'logs.partial_tail',
                'Los registros se analizaron parcialmente',
                'información',
                'informativa',
                'Para limitar memoria se leyeron únicamente los últimos 512 KiB de al menos un registro.',
                'Los errores antiguos pueden no aparecer en el informe.',
                array('Reproduce el fallo una vez y vuelve a ejecutar el diagnóstico.', 'Conserva una copia completa del registro si necesitas una investigación histórica.'),
                'Comprueba si el error reciente aparece en el nuevo informe.',
                'parcial'
            );
        }
        return CheckResult::complete($findings, $partial ? 'parcial' : null, 'Registros locales analizados: ' . $read . '.');
    }

    private function tail($path, $limit)
    {
        $size = (int) @filesize($path);
        $handle = @fopen($path, 'rb');
        if ($handle === false) {
            return array('', false);
        }
        $partial = $size > $limit;
        if ($partial) {
            @fseek($handle, -$limit, SEEK_END);
        }
        $contents = stream_get_contents($handle, $limit);
        fclose($handle);
        return array(is_string($contents) ? $contents : '', $partial);
    }

    private function classify($line)
    {
        if (stripos($line, 'Allowed memory size') !== false) {
            return 'memory';
        }
        if (stripos($line, 'Parse error') !== false) {
            return 'parse';
        }
        if (stripos($line, 'Access denied for user') !== false) {
            return 'database';
        }
        if (stripos($line, 'Call to undefined function') !== false || stripos($line, 'Failed opening required') !== false) {
            return 'dependency';
        }
        return 'fatal';
    }

    private function finding($type, $evidence)
    {
        $map = array(
            'memory' => array('WordPress agotó la memoria disponible', 'PHP detuvo la ejecución al superar memory_limit.', array('Haz una copia de seguridad.', 'Aumenta el límite desde el panel del alojamiento.', 'Si el error menciona un plugin concreto, prueba a desactivarlo manualmente renombrando solo su carpeta después de guardar una copia.'), 'Recarga el sitio y comprueba que no se repite el error de memoria.'),
            'parse' => array('Existe un error de sintaxis PHP', 'PHP no puede interpretar uno de los archivos cargados.', array('Identifica en la evidencia el archivo y la línea.', 'Restaura ese archivo desde una copia conocida o desde la misma versión oficial del componente.', 'No corrijas a ciegas un archivo si puede haber sido manipulado.'), 'Recarga el sitio y revisa que el registro no añada otro Parse error.'),
            'database' => array('La base de datos rechaza las credenciales', 'El servidor MySQL ha denegado el acceso que intenta usar WordPress.', array('Compara DB_NAME, DB_USER, DB_HOST y DB_PASSWORD con los datos del panel.', 'No publiques ni pegues la contraseña en un informe.', 'Actualiza wp-config.php solo después de guardar una copia.'), 'Usa de nuevo la prueba de conexión de esta herramienta.'),
            'dependency' => array('Falta una función o archivo requerido', 'Un componente intenta cargar código que no está disponible.', array('Identifica el plugin, tema o archivo citado.', 'Comprueba que la subida o actualización se completó.', 'Restaura el componente desde una copia oficial de la misma versión.'), 'Recarga el sitio y confirma que no vuelve a registrarse el error.'),
            'fatal' => array('El registro contiene un error fatal reciente', 'PHP detuvo WordPress antes de completar la respuesta.', array('Identifica en la evidencia el archivo responsable.', 'Haz una copia antes de cambiarlo.', 'Si pertenece a un plugin, renombra únicamente su carpeta para una prueba temporal; si pertenece al núcleo, restaura la misma versión oficial.'), 'Recarga el sitio y comprueba el registro inmediatamente después.'),
        );
        $item = $map[$type];
        return new Finding('logs.' . $type, $item[0], 'problema confirmado', 'crítica', $evidence, $item[1], $item[2], $item[3]);
    }
}
