<?php

/**
 * SystemController
 *
 * Centralises all data-fetching for the admin system page and its
 * fetch()-based tab-panel fragment endpoint.
 *
 * Responsibilities:
 *  - System status checks (nginx, php-fpm, HTTP ping, database, storage,
 *    maintenance mode) with SystemCache TTL caching
 *  - PHP environment info (1-hour TTL)
 *  - Disk usage info (5-minute TTL)
 *  - Log file reading (tail, meta)
 *  - Nginx config file reading
 *  - Log/nginx line classifiers used by both system.php and system-fragment.php
 *
 * Both system.php (full page) and system-fragment.php (AJAX panel) delegate
 * here so helpers are never duplicated.
 */
class SystemController
{
    // ── Constants ─────────────────────────────────────────────────────────────

    public const LOG_FILES = [
        'nginx_access' => ['label' => 'nginx — accès',     'path' => '/var/log/nginx/xamxam_access.log'],
        'nginx_error'  => ['label' => 'nginx — erreurs',   'path' => '/var/log/nginx/xamxam_error.log'],
        'php_error'    => ['label' => 'PHP-FPM — erreurs', 'path' => '/var/log/php8.4-fpm.log'],
    ];

    public const ALLOWED_LINES = [50, 100, 200, 500];

    /** Live deployed nginx config path. */
    public const NGINX_CONFIG_LIVE  = '/etc/nginx/sites-available/xamxam';
    /** Local reference copy used as fallback in dev. */
    public const NGINX_CONFIG_LOCAL = APP_ROOT . '/nginx/xamxam.conf';

    // ── TTLs ──────────────────────────────────────────────────────────────────
    private const TTL_STATUS = 120;   // 2 minutes
    private const TTL_PHP    = 3600;  // 1 hour
    private const TTL_DISK   = 300;   // 5 minutes

    private Database    $db;
    private SystemCache $cache;

    public function __construct(Database $db, SystemCache $cache)
    {
        $this->db    = $db;
        $this->cache = $cache;
    }

    // ── Cache invalidation ────────────────────────────────────────────────────

    /**
     * Force-bust all cached sections (called on ?refresh=1).
     */
    public function invalidateAll(): void
    {
        $this->cache->invalidate('system_status');
        $this->cache->invalidate('disk_info');
        $this->cache->invalidate('php_info');
    }

    // ── Status data ───────────────────────────────────────────────────────────

    /**
     * Return system status checks array, from cache when fresh.
     *
     * @return array{checks: array, cached: bool, cacheAge: ?int}
     */
    public function getStatusData(): array
    {
        $cacheAge = $this->cache->ageSeconds('system_status');
        $cached   = $this->cache->get('system_status', self::TTL_STATUS);

        if ($cached !== null) {
            return ['checks' => $cached, 'cached' => true, 'cacheAge' => $cacheAge];
        }

        $checks = $this->runStatusChecks();
        $this->cache->set('system_status', $checks);

        return ['checks' => $checks, 'cached' => false, 'cacheAge' => 0];
    }

    /**
     * Return PHP environment info, from cache when fresh.
     *
     * @return array<string, string>
     */
    public function getPhpInfo(): array
    {
        $cached = $this->cache->get('php_info', self::TTL_PHP);
        if ($cached !== null) {
            return $cached;
        }

        $info = [
            'version'      => PHP_VERSION,
            'sapi'         => PHP_SAPI,
            'memory_limit' => (string) ini_get('memory_limit'),
            'upload_max'   => (string) ini_get('upload_max_filesize'),
            'post_max'     => (string) ini_get('post_max_size'),
            'max_exec'     => ini_get('max_execution_time') . 's',
        ];
        $this->cache->set('php_info', $info);

        return $info;
    }

    /**
     * Return disk usage info, from cache when fresh.
     *
     * @return array{total: int, free: int, used: int, pct: int}
     */
    public function getDiskInfo(): array
    {
        $cached = $this->cache->get('disk_info', self::TTL_DISK);
        if ($cached !== null) {
            return $cached;
        }

        $total = (int) disk_total_space(APP_ROOT);
        $free  = (int) disk_free_space(APP_ROOT);
        $used  = $total - $free;
        $pct   = $total > 0 ? (int) round((float) $used / (float) $total * 100.0) : 0;

        $info = ['total' => $total, 'free' => $free, 'used' => $used, 'pct' => $pct];
        $this->cache->set('disk_info', $info);

        return $info;
    }

    // ── Log tab ───────────────────────────────────────────────────────────────

    /**
     * Read and return data for a log tab.
     *
     * @return array{lines: ?array, error: ?string, meta: ?array}
     */
    public function getLogData(string $tab, int $n): array
    {
        $logPath  = self::LOG_FILES[$tab]['path'];
        $error    = null;
        $lines    = $this->readLogTail($logPath, $n, $error);
        $meta     = null;

        if (file_exists($logPath)) {
            $sz   = filesize($logPath);
            $meta = [
                'size'  => $sz > 1048576
                    ? number_format($sz / 1048576, 2) . ' MB'
                    : number_format($sz / 1024, 1) . ' KB',
                'mtime' => date('d/m/Y H:i:s', filemtime($logPath)),
                'path'  => $logPath,
            ];
        }

        return ['lines' => $lines, 'error' => $error, 'meta' => $meta];
    }

    // ── Nginx config tab ──────────────────────────────────────────────────────

    /**
     * Read and return data for the nginx config tab.
     *
     * @return array{lines: ?array, source: ?string, meta: ?array, error: ?string}
     */
    public function getNginxConfigData(): array
    {
        $livePath  = self::NGINX_CONFIG_LIVE;
        $localPath = self::NGINX_CONFIG_LOCAL;

        foreach ([[$livePath, 'live'], [$localPath, 'local']] as [$path, $src]) {
            if (file_exists($path) && is_readable($path)) {
                $raw = file($path, FILE_IGNORE_NEW_LINES);
                if ($raw !== false) {
                    $sz   = filesize($path);
                    $meta = [
                        'path'  => $path,
                        'size'  => $sz > 1048576
                            ? number_format($sz / 1048576, 2) . ' MB'
                            : number_format($sz / 1024, 1) . ' KB',
                        'mtime' => date('d/m/Y H:i:s', filemtime($path)),
                    ];
                    return ['lines' => $raw, 'source' => $src, 'meta' => $meta, 'error' => null];
                }
            }
        }

        $error = file_exists($livePath)
            ? 'Fichier non lisible (permissions insuffisantes) : ' . htmlspecialchars($livePath)
            : 'Config live introuvable (' . htmlspecialchars($livePath) . ') et config locale introuvable (' . htmlspecialchars($localPath) . ').';

        return ['lines' => null, 'source' => null, 'meta' => null, 'error' => $error];
    }

    // ── Line classifiers (used by both system.php and system-fragment.php) ────

    /**
     * Return the CSS class for a log line.
     */
    public static function logLineClass(string $line): string
    {
        if (preg_match('/\[(crit|emerg|alert)\]/', $line)) {
            return 'log-crit';
        }
        if (preg_match('/\[error\]/', $line)) {
            return 'log-error';
        }
        if (preg_match('/\[warn\]/', $line)) {
            return 'log-warn';
        }
        if (preg_match('/\[notice\]/', $line)) {
            return 'log-notice';
        }
        if (preg_match('/" [45]\d\d /', $line)) {
            return 'log-error';
        }
        if (preg_match('/" 3\d\d /', $line)) {
            return 'log-notice';
        }
        return '';
    }

    /**
     * Return the CSS class for a nginx config line.
     */
    public static function nginxLineClass(string $line): string
    {
        $trimmed = ltrim($line);
        if ($trimmed === '' || str_starts_with($trimmed, '#')) {
            return 'nginx-comment';
        }
        if (preg_match('/^\s*(location|server|upstream|events|http|geo|map|types)\b/', $line)) {
            return 'nginx-block';
        }
        return 'nginx-directive';
    }

    // ── View helpers ──────────────────────────────────────────────────────────

    /**
     * Human-readable byte string (GB / MB / KB).
     */
    public static function humanBytes(int $bytes): string
    {
        if ($bytes > 1073741824) {
            return number_format($bytes / 1073741824, 1) . ' GB';
        }
        if ($bytes > 1048576) {
            return number_format($bytes / 1048576, 1)    . ' MB';
        }
        return number_format($bytes / 1024, 1) . ' KB';
    }

    /**
     * French status label with leading symbol.
     */
    public static function statusLabel(string $status): string
    {
        return match ($status) {
            'active'   => '● En ligne',
            'inactive' => '○ Inactif',
            'failed'   => '✕ Erreur',
            'warn'     => '⚠ Attention',
            default    => '? Inconnu',
        };
    }

    /**
     * CSS class for a status value.
     */
    public static function statusClass(string $status): string
    {
        return match ($status) {
            'active'   => 'status-ok',
            'inactive' => 'status-warn',
            'warn'     => 'status-warn',
            'failed'   => 'status-err',
            default    => 'status-unknown',
        };
    }

    /**
     * CSS colour string for a disk-usage percentage.
     */
    public static function diskColor(int $pct): string
    {
        if ($pct > 85) {
            return '#e05555';
        }
        if ($pct > 70) {
            return '#ffc107';
        }
        return '#4caf50';
    }

    // ── Private helpers ───────────────────────────────────────────────────────

    /**
     * Execute all six status checks and return the checks array.
     */
    private function runStatusChecks(): array
    {
        $checks = [];

        // nginx
        $nginxStatus  = $this->systemdStatus('nginx');
        $nginxVersion = $this->safeExec('nginx -v 2>&1 | head -1');
        $checks['nginx'] = [
            'label'  => 'nginx',
            'status' => $nginxStatus,
            'detail' => $nginxVersion,
        ];

        // php-fpm — probe running PHP version's unit first, then fall back
        $phpFpmStatus  = null;
        $phpFpmUnit    = null;
        $phpMajMin     = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;
        $fpmCandidates = array_unique([
            'php' . $phpMajMin . '-fpm',
            'php8.4-fpm', 'php8.3-fpm', 'php8.2-fpm', 'php8.1-fpm', 'php-fpm',
        ]);
        foreach ($fpmCandidates as $unit) {
            $s = $this->systemdStatus($unit);
            if ($s !== null && $s !== 'unknown') {
                $phpFpmStatus = $s;
                $phpFpmUnit   = $unit;
                break;
            }
        }
        $checks['php_fpm'] = [
            'label'  => 'php-fpm' . ($phpFpmUnit ? " ($phpFpmUnit)" : ''),
            'status' => $phpFpmStatus,
            'detail' => null,
        ];

        // Site HTTP ping
        $siteUrl    = (isset($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . '/';
        $httpResult = $this->localHttpCheck($siteUrl);
        $checks['site_http'] = [
            'label'  => 'Site HTTP',
            'status' => $httpResult !== null ? ($httpResult[0] < 500 ? 'active' : 'failed') : null,
            'detail' => $httpResult !== null
                ? "HTTP {$httpResult[0]} — {$httpResult[1]} ms"
                : 'curl indisponible',
        ];

        // Database
        $dbPath      = $this->db->getDatabasePath();
        $dbExists    = file_exists($dbPath);
        $dbWritable  = $dbExists && is_writable($dbPath);
        $dbSizeBytes = $dbExists ? filesize($dbPath) : null;
        $dbSizeHuman = $dbSizeBytes !== null
            ? ($dbSizeBytes > 1048576
                ? number_format($dbSizeBytes / 1048576, 1) . ' MB'
                : number_format($dbSizeBytes / 1024, 1) . ' KB')
            : 'N/A';
        $dbRowCount = null;
        if ($dbExists) {
            try {
                $dbRowCount = $this->db->getThesisCount();
            } catch (Throwable) {
            }
        }
        $checks['database'] = [
            'label'  => 'Base de données SQLite',
            'status' => $dbExists ? ($dbWritable ? 'active' : 'inactive') : 'failed',
            'detail' => $dbExists
                ? ($dbRowCount !== null ? "$dbRowCount thèses — $dbSizeHuman" : "Lecture impossible — $dbSizeHuman")
                : 'Fichier introuvable',
        ];

        // Storage directory
        $storageDir      = APP_ROOT . '/storage';
        $storageWritable = is_dir($storageDir) && is_writable($storageDir);
        $coversDir  = $storageDir . '/covers';
        $checks['storage'] = [
            'label'  => 'Répertoire storage',
            'status' => $storageWritable ? 'active' : (is_dir($storageDir) ? 'inactive' : 'failed'),
            'detail' => $storageWritable
                ? implode('  ·  ', array_filter([
                    is_dir($coversDir) ? ('covers/ '  . count(array_diff(scandir($coversDir), ['.','..'])) . ' fichiers') : null,
                  ]))
                : 'Non accessible en écriture',
        ];

        // Maintenance mode
        $maintenanceOn = file_exists(APP_ROOT . '/storage/maintenance.flag');
        $checks['maintenance'] = [
            'label'  => 'Mode maintenance',
            'status' => $maintenanceOn ? 'warn' : 'active',
            'detail' => $maintenanceOn ? 'Activé — site public inaccessible' : 'Désactivé',
        ];

        return $checks;
    }

    /**
     * Read the tail of a log file, newest-first. Returns null on error.
     */
    private function readLogTail(string $logPath, int $n, ?string &$errorMsg): ?array
    {
        $errorMsg = null;

        if (!function_exists('exec')) {
            $errorMsg = 'exec() est désactivé sur ce serveur.';
            return null;
        }
        if (!file_exists($logPath)) {
            $errorMsg = 'Fichier introuvable : ' . htmlspecialchars($logPath);
            return null;
        }
        if (!is_readable($logPath)) {
            $errorMsg = 'Fichier non lisible (permissions insuffisantes) : ' . htmlspecialchars($logPath);
            return null;
        }

        $output = [];
        $rc     = 0;
        exec('tail -n ' . intval($n) . ' ' . escapeshellarg($logPath) . ' 2>/dev/null', $output, $rc);

        if ($rc !== 0) {
            $errorMsg = 'Erreur lors de la lecture du fichier journal.';
            return null;
        }

        return array_reverse($output); // newest first
    }

    /**
     * Run a shell command safely, returning trimmed stdout or null on failure.
     */
    private function safeExec(string $cmd): ?string
    {
        if (!function_exists('exec')) {
            return null;
        }
        $output = [];
        $rc     = 0;
        exec($cmd . ' 2>/dev/null', $output, $rc);
        return $rc === 0 ? trim(implode("\n", $output)) : null;
    }

    /**
     * Query systemd for a unit's active state.
     */
    private function systemdStatus(string $unit): ?string
    {
        $raw = $this->safeExec('systemctl is-active ' . escapeshellarg($unit));
        if ($raw === null) {
            return null;
        }
        return in_array($raw, ['active', 'inactive', 'failed', 'activating', 'deactivating'], true)
            ? $raw : 'unknown';
    }

    /**
     * Perform a lightweight HEAD request to $url and return [httpCode, ms].
     * Returns null if curl is unavailable.
     */
    private function localHttpCheck(string $url): ?array
    {
        if (!function_exists('curl_init')) {
            return null;
        }
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_NOBODY         => true,
            CURLOPT_TIMEOUT        => 5,
            CURLOPT_CONNECTTIMEOUT => 3,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => 0,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS      => 3,
        ]);
        $start = microtime(true);
        curl_exec($ch);
        $ms   = (int) round((microtime(true) - $start) * 1000.0);
        $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        return $code > 0 ? [$code, $ms] : null;
    }
}
