#!/usr/bin/env php
<?php
/**
 * Environment Doctor CLI
 * Usage:
 *   php bin/doctor
 *   php bin/doctor --strict
 */

$basePath = dirname(__DIR__);
define('BASE_PATH', $basePath . '/');

$args = array_slice($argv, 1);
$strictMode = in_array('--strict', $args, true) || in_array('-s', $args, true);

if (in_array('--help', $args, true) || in_array('-h', $args, true)) {
    echo "\nKlee Doctor\n";
    echo "Uso:\n";
    echo "  php bin/doctor           Validación general de entorno\n";
    echo "  php bin/doctor --strict  Falla también por warnings (ideal CI)\n\n";
    exit(0);
}

require $basePath . '/core/helpers/Env.php';
Env::load($basePath . '/.env');

if (file_exists($basePath . '/app/config/Config.php')) {
    require $basePath . '/app/config/Config.php';
}
if (file_exists($basePath . '/app/config/ConfigEnv.php')) {
    require $basePath . '/app/config/ConfigEnv.php';
}

$errors = 0;
$warnings = 0;

$print = function ($type, $message) use (&$errors, &$warnings) {
    if ($type === 'ERROR') {
        $errors++;
        echo "[error] {$message}\n";
        return;
    }

    if ($type === 'WARN') {
        $warnings++;
        echo "[warn] {$message}\n";
        return;
    }

    echo "[ok] {$message}\n";
};

echo "\nKlee Doctor\n";
echo str_repeat('-', 60) . "\n";

// 1) Config files
$configPath = $basePath . '/app/config/Config.php';
$configEnvPath = $basePath . '/app/config/ConfigEnv.php';

if (is_file($configPath)) {
    $print('OK', 'Config.php encontrado.');
} else {
    $print('ERROR', 'Falta app/config/Config.php');
}

if (is_file($configEnvPath)) {
    $print('OK', 'ConfigEnv.php encontrado.');
} else {
    $print('ERROR', 'Falta app/config/ConfigEnv.php (copiar desde ConfigEnv.example.php).');
}

if (is_file($basePath . '/.env')) {
    $print('OK', '.env encontrado.');
} else {
    $print('WARN', '.env no encontrado (usa .env.example como plantilla).');
}

// 2) Env variables (soft requirement)
$requiredEnv = array('DB_DRIVER', 'DB_HOST', 'DB_NAME', 'DB_USER');
$missingEnv = array();
foreach ($requiredEnv as $envKey) {
    if (getenv($envKey) === false || trim((string)getenv($envKey)) === '') {
        $missingEnv[] = $envKey;
    }
}

if (empty($missingEnv)) {
    $print('OK', 'Variables de entorno base de DB presentes.');
} else {
    $print('WARN', 'Variables de entorno faltantes: ' . implode(', ', $missingEnv));
}

// 2.1) SMTP variables
$mailHost = trim((string)(getenv('MAIL_HOST') ?: ''));
$mailUser = trim((string)(getenv('MAIL_USER') ?: ''));
$mailPassRaw = getenv('MAIL_PASS');
$mailPass = $mailPassRaw === false ? '' : (string)$mailPassRaw;
$mailPort = trim((string)(getenv('MAIL_PORT') ?: ''));
$mailEncryption = strtolower(trim((string)(getenv('MAIL_ENCRYPTION') ?: '')));

if ($mailHost !== '') {
    $smtpErrors = array();
    $smtpWarnings = array();

    if ($mailPort === '' || !ctype_digit($mailPort)) {
        $smtpErrors[] = 'MAIL_PORT inválido (debe ser numérico).';
    }

    if ($mailEncryption !== '' && !in_array($mailEncryption, array('tls', 'ssl', 'starttls', 'none'), true)) {
        $smtpErrors[] = 'MAIL_ENCRYPTION inválido (use tls|ssl|starttls|none).';
    }

    if ($mailUser === '') {
        $smtpWarnings[] = 'MAIL_USER vacío.';
    }

    if ($mailPassRaw === false || $mailPass === '') {
        $smtpWarnings[] = 'MAIL_PASS vacío.';
    }

    if (empty($smtpErrors) && empty($smtpWarnings)) {
        $print('OK', 'Configuración SMTP base presente.');
    } else {
        foreach ($smtpErrors as $msg) {
            $print('ERROR', $msg);
        }
        foreach ($smtpWarnings as $msg) {
            $print('WARN', $msg);
        }
    }
} else {
    $print('WARN', 'MAIL_HOST no definido; envío SMTP quedará deshabilitado o incompleto.');
}

// 3) PHP/extensiones
$print('OK', 'PHP version: ' . PHP_VERSION);
$requiredExt = array('gd', 'mbstring', 'pdo', 'pdo_mysql', 'openssl');
foreach ($requiredExt as $ext) {
    if (extension_loaded($ext)) {
        $print('OK', "Extensión {$ext} cargada.");
    } else {
        $print('ERROR', "Extensión {$ext} no cargada.");
    }
}

// 4) Writable directories
$writableDirs = array(
    $basePath . '/storage',
    $basePath . '/storage/cache',
    $basePath . '/logs',
    $basePath . '/files',
);

foreach ($writableDirs as $dir) {
    if (!is_dir($dir)) {
        $print('WARN', "Directorio no existe: {$dir}");
        continue;
    }

    if (is_writable($dir)) {
        $print('OK', "Directorio escribible: {$dir}");
    } else {
        $print('ERROR', "Directorio sin permiso de escritura: {$dir}");
    }
}

// 4b) Subidas de archivos: el usuario del servidor web debe poder escribir en
// storage/ y los limites de PHP deben alcanzar para los adjuntos. Un fallo aqui
// se traduce en tickets creados sin sus adjuntos.
$parseIniSize = function ($value) {
    $value = trim((string)$value);
    if ($value === '') {
        return 0;
    }

    $unit = strtolower(substr($value, -1));
    $number = (float)$value;
    if ($unit === 'g') {
        $number *= 1024 * 1024 * 1024;
    } elseif ($unit === 'm') {
        $number *= 1024 * 1024;
    } elseif ($unit === 'k') {
        $number *= 1024;
    }

    return (int)$number;
};

$uploadMax = $parseIniSize(ini_get('upload_max_filesize'));
$postMax = $parseIniSize(ini_get('post_max_size'));

if (!ini_get('file_uploads')) {
    $print('ERROR', 'file_uploads está desactivado en PHP: ningún adjunto puede subirse.');
} elseif ($uploadMax < (10 * 1024 * 1024)) {
    $print('WARN', 'upload_max_filesize = ' . ini_get('upload_max_filesize')
        . ' (bajo para adjuntos de mesa de ayuda; se recomienda 50M junto con post_max_size 55M).');
} else {
    $print('OK', 'Límites de subida: upload_max_filesize=' . ini_get('upload_max_filesize')
        . ', post_max_size=' . ini_get('post_max_size') . '.');
}

if ($postMax > 0 && $uploadMax > 0 && $postMax <= $uploadMax) {
    $print('WARN', 'post_max_size (' . ini_get('post_max_size') . ') no supera upload_max_filesize ('
        . ini_get('upload_max_filesize') . '): el formulario completo puede rechazarse.');
}

$storageDir = $basePath . '/storage';
$webUsers = array('www-data', 'apache', 'httpd', 'http', 'nginx');
$webUser = null;
if (function_exists('posix_getpwnam')) {
    foreach ($webUsers as $candidate) {
        if (posix_getpwnam($candidate) !== false) {
            $webUser = $candidate;
            break;
        }
    }
}

if ($webUser === null) {
    $print('WARN', 'No se identificó el usuario del servidor web: verifique a mano que pueda escribir en storage/.');
} elseif (!is_dir($storageDir)) {
    $print('ERROR', 'No existe el directorio storage/.');
} else {
    $probe = $storageDir . '/.doctor_web_probe';
    $command = 'sudo -n -u ' . escapeshellarg($webUser) . ' touch ' . escapeshellarg($probe) . ' 2>&1';
    $output = trim((string)@shell_exec($command));
    $canTest = is_file($probe);

    if ($canTest) {
        @unlink($probe);
        $print('OK', "El usuario {$webUser} puede escribir en storage/.");
    } else {
        // Sin sudo no se puede probar la escritura real: se infiere a partir de
        // los permisos declarados y solo se marca ERROR cuando la evidencia es
        // concluyente (herramientas disponibles y ningún permiso otorgado).
        $aclAvailable = (trim((string)@shell_exec('command -v getfacl 2>/dev/null')) !== '');
        $acl = $aclAvailable
            ? trim((string)@shell_exec('getfacl -p ' . escapeshellarg($storageDir) . ' 2>/dev/null'))
            : '';
        $hasAcl = ($acl !== '' && strpos($acl, 'user:' . $webUser . ':rw') !== false);

        $perms = (int)@fileperms($storageDir);
        $worldWritable = (($perms & 0002) !== 0);
        $groupWritable = (($perms & 0020) !== 0);

        $ownerName = '';
        $groupName = '';
        if (function_exists('posix_getpwuid')) {
            $ownerInfo = @posix_getpwuid(@fileowner($storageDir));
            $ownerName = is_array($ownerInfo) ? (string)($ownerInfo['name'] ?? '') : '';
        }
        if (function_exists('posix_getgrgid')) {
            $groupInfo = @posix_getgrgid(@filegroup($storageDir));
            $groupName = is_array($groupInfo) ? (string)($groupInfo['name'] ?? '') : '';
        }

        $ownedByWeb = ($ownerName === $webUser);
        $groupIsWeb = ($groupWritable && in_array($groupName, $webUsers, true));

        if ($hasAcl || $worldWritable || $ownedByWeb || $groupIsWeb) {
            $print('WARN', "storage/ tiene permisos para {$webUser}, pero no se pudo comprobar la escritura real"
                . " (sin sudo). Si Apache/PHP-FPM corre con systemd, revise ProtectHome/ReadWritePaths.");
        } elseif ($aclAvailable) {
            $print('ERROR', "storage/ no es escribible por {$webUser}: los adjuntos e imágenes de tickets se descartan."
                . " Otorgue permisos (setfacl -R -m u:{$webUser}:rwX storage) y, si el servicio usa systemd con"
                . " ProtectHome/ProtectSystem, agregue ReadWritePaths={$storageDir}.");
        } else {
            $print('WARN', "No se pudo determinar si {$webUser} escribe en storage/ (sin sudo ni getfacl)."
                . " Verifíquelo a mano: los adjuntos de tickets dependen de ello.");
        }

        if ($output !== '' && stripos($output, 'sudo') === false) {
            $print('WARN', 'Detalle de la prueba de escritura: ' . $output);
        }
    }
}

// 5) Classmap integrity
$classmapFile = $basePath . '/storage/cache/classmap.php';
if (!is_file($classmapFile)) {
    $print('WARN', 'classmap.php no encontrado (ejecuta php bin/cache rebuild para reconstruir).');
} else {
    $classmap = include $classmapFile;
    if (!is_array($classmap)) {
        $print('ERROR', 'classmap.php inválido (no retorna array).');
    } else {
        $basePathReal = realpath($basePath);
        $invalid = 0;

        foreach ($classmap as $className => $classPath) {
            if (!is_string($className) || !is_string($classPath) || !file_exists($classPath)) {
                $invalid++;
                continue;
            }

            $classPathReal = realpath($classPath);
            if ($classPathReal === false || $basePathReal === false) {
                $invalid++;
                continue;
            }

            $startsWith = (DIRECTORY_SEPARATOR === '\\')
                ? stripos($classPathReal, $basePathReal) === 0
                : strpos($classPathReal, $basePathReal) === 0;

            if (!$startsWith) {
                $invalid++;
            }
        }

        if ($invalid === 0) {
            $print('OK', 'Classmap íntegro.');
        } else {
            $print('WARN', "Classmap con {$invalid} entrada(s) inválida(s).");
        }
    }
}

// 6) DB connectivity
$dbConfig = null;
if (class_exists('ConfigEnv') && property_exists('ConfigEnv', 'DB_CONNECTIONS') && isset(ConfigEnv::$DB_CONNECTIONS['klee'])) {
    $dbConfig = ConfigEnv::$DB_CONNECTIONS['klee'];
}

if (!$dbConfig) {
    $dbConfig = array(
        'driver' => getenv('DB_DRIVER') ?: 'mysql',
        'host' => getenv('DB_HOST') ?: '',
        'dbname' => getenv('DB_NAME') ?: '',
        'user' => getenv('DB_USER') ?: '',
        'password' => getenv('DB_PASS') ?: '',
    );
}

if (empty($dbConfig['host']) || empty($dbConfig['dbname']) || empty($dbConfig['user'])) {
    $print('ERROR', 'Configuración de DB incompleta (host/dbname/user).');
} else {
    try {
        if (($dbConfig['driver'] ?? 'mysql') !== 'mysql') {
            throw new RuntimeException('Driver no soportado por doctor: ' . ($dbConfig['driver'] ?? 'null'));
        }

        $dsn = "mysql:host={$dbConfig['host']};dbname={$dbConfig['dbname']};charset=utf8mb4";
        $pdo = new PDO($dsn, $dbConfig['user'], $dbConfig['password'] ?? '');
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $pdo->query('SELECT 1');
        $print('OK', 'Conexión DB correcta.');

        $ticketsTable = $pdo->query(
            "SELECT COUNT(*) FROM information_schema.tables "
            . "WHERE table_schema = DATABASE() AND table_name = 'tickets'"
        )->fetchColumn();
        if ((int)$ticketsTable > 0) {
            $sourceRow = $pdo->query(
                "SELECT COUNT(*) AS Total, "
                . "SUM(CASE WHEN Descripcion LIKE '%cargada desde seeder demo%' THEN 1 ELSE 0 END) AS Demo "
                . "FROM tickets WHERE Estado = 1"
            )->fetch(PDO::FETCH_ASSOC);
            $totalTickets = (int)($sourceRow['Total'] ?? 0);
            $demoTickets = (int)($sourceRow['Demo'] ?? 0);

            if ($demoTickets > 0 && $demoTickets === $totalTickets) {
                $print(
                    'WARN',
                    "Fuente de tickets solo DEMO: {$demoTickets}/{$totalTickets}. Los reportes se identificarán como demostración."
                );
            } elseif ($demoTickets > 0) {
                $print(
                    'WARN',
                    "Fuente de tickets mezclada: {$demoTickets} demo de {$totalTickets}. Los reportes indicarán que incluyen datos demo."
                );
            } else {
                $print('OK', "Fuente de tickets sin marcadores demo: {$totalTickets} caso(s) activo(s).");
            }
        }
    } catch (Exception $e) {
        $print('ERROR', 'Conexión DB falló: ' . $e->getMessage());
    }
}

echo str_repeat('-', 60) . "\n";
echo "Resumen: errors={$errors} warnings={$warnings}\n";

if ($strictMode) {
    echo "Modo: strict\n";
} else {
    echo "Modo: normal\n";
}

if ($errors > 0) {
    exit(2);
}
if ($warnings > 0 && $strictMode) {
    exit(1);
}
exit(0);
