<?php

declare(strict_types=1);

namespace App\Services\Install;

/**
 * SystemDetector — OS / 패키지 매니저 / PHP 빌드 자동 감지.
 *
 * 인스톨러 환경 점검에서 누락된 PHP 확장의 정확한 설치 명령을 생성하기 위한 분류기.
 *
 * 출력 키:
 *   - os                  ubuntu | debian | centos | rhel | rocky | alma | alpine | arch | unknown
 *   - os_version          22.04 | 12 | 9 | 3.19 | etc.
 *   - package_manager     apt | dnf | yum | apk | pacman | null
 *   - php_build           debian-default | sury | aapanel | source | unknown
 *   - php_version         e.g. 8.3.10
 *   - php_ini_dir         /etc/php/8.3/fpm/conf.d  | /www/server/php/83/etc/php.d ...
 *   - php_extension_dir   /usr/lib/php/20230831 ...
 *   - php_fpm_service     php8.3-fpm | php-fpm-83 | php-fpm
 *   - has_sudo            bool — sudo -n true 통과 여부
 *   - is_root             bool — posix_getuid() === 0
 *   - web_user            www-data | nginx | apache | www
 *
 * 보안: shell_exec 가 disabled 인 경우에도 안전한 fallback.
 */
final class SystemDetector
{
    /** @var array<string,mixed>|null */
    private ?array $cache = null;

    /**
     * @return array<string,mixed>
     */
    public function detect(): array
    {
        if ($this->cache !== null) {
            return $this->cache;
        }

        $os = $this->detectOs();
        $osVersion = $this->detectOsVersion();
        $pm = $this->detectPackageManager();
        $build = $this->detectPhpBuild();

        return $this->cache = [
            'os'                 => $os,
            'os_version'         => $osVersion,
            'package_manager'    => $pm,
            'php_build'          => $build,
            'php_version'        => PHP_VERSION,
            'php_major_minor'    => PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION,
            'php_ini_dir'        => $this->getPhpIniDir(),
            'php_extension_dir' => $this->getExtensionDir(),
            'php_fpm_service'    => $this->detectFpmService($os, $build),
            'has_sudo'           => $this->canSudoNoPass(),
            'is_root'            => function_exists('posix_getuid') && posix_getuid() === 0,
            'web_user'           => $this->detectWebUser($os),
            'shell_exec_enabled' => function_exists('shell_exec') && ! $this->isFunctionDisabled('shell_exec'),
        ];
    }

    private function detectOs(): string
    {
        $osRelease = @file_get_contents('/etc/os-release');
        if (is_string($osRelease) && preg_match('/^ID=("?)([a-z0-9_\-]+)\1/mi', $osRelease, $m)) {
            $id = strtolower($m[2]);
            // Normalize common variants
            return match ($id) {
                'rhel', 'redhat', 'red-hat-enterprise-linux' => 'rhel',
                'centos' => 'centos',
                'rocky' => 'rocky',
                'almalinux', 'alma' => 'alma',
                'ubuntu' => 'ubuntu',
                'debian' => 'debian',
                'alpine' => 'alpine',
                'arch', 'archlinux' => 'arch',
                'fedora' => 'fedora',
                default => $id,
            };
        }
        return 'unknown';
    }

    private function detectOsVersion(): string
    {
        $osRelease = @file_get_contents('/etc/os-release');
        if (is_string($osRelease) && preg_match('/^VERSION_ID=("?)([0-9.]+)\1/mi', $osRelease, $m)) {
            return $m[2];
        }
        return '';
    }

    private function detectPackageManager(): ?string
    {
        $map = [
            'apt-get' => 'apt',
            'dnf'     => 'dnf',
            'yum'     => 'yum',
            'apk'     => 'apk',
            'pacman'  => 'pacman',
        ];
        foreach ($map as $cmd => $name) {
            if ($this->commandExists($cmd)) {
                return $name;
            }
        }
        return null;
    }

    private function detectPhpBuild(): string
    {
        // aaPanel: /www/server/php/{version}/
        if (is_dir('/www/server/php')) {
            return 'aapanel';
        }
        // Sury PPA (Ubuntu/Debian)
        $suryFiles = [
            '/etc/apt/sources.list.d/sury-php.list',
            '/etc/apt/sources.list.d/php.list',
        ];
        foreach ($suryFiles as $f) {
            if (file_exists($f)) {
                $c = @file_get_contents($f);
                if (is_string($c) && (str_contains($c, 'sury.org') || str_contains($c, 'ondrej'))) {
                    return 'sury';
                }
            }
        }
        // Glob for ondrej PPA on Ubuntu
        $glob = glob('/etc/apt/sources.list.d/ondrej-ubuntu-php-*.list') ?: [];
        if (! empty($glob)) {
            return 'sury';
        }
        // Debian/Ubuntu default
        if (is_dir('/etc/php')) {
            return 'debian-default';
        }
        // RHEL family default location
        if (is_dir('/etc/php.d') || is_file('/etc/php.ini')) {
            return 'rhel-default';
        }
        return 'source';
    }

    private function getPhpIniDir(): string
    {
        $loaded = php_ini_loaded_file() ?: '';
        if ($loaded !== '') {
            // .d sibling
            $dir = dirname($loaded);
            $confD = $dir.'/conf.d';
            if (is_dir($confD)) {
                return $confD;
            }
            return $dir;
        }
        $scanned = php_ini_scanned_files();
        if (is_string($scanned) && $scanned !== '') {
            $first = trim(explode(',', $scanned)[0]);
            if ($first !== '') {
                return dirname($first);
            }
        }
        return '';
    }

    private function getExtensionDir(): string
    {
        return (string) ini_get('extension_dir');
    }

    private function detectFpmService(string $os, string $build): string
    {
        $mm = PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION;
        $mmNoDot = PHP_MAJOR_VERSION.PHP_MINOR_VERSION;

        if ($build === 'aapanel') {
            // aaPanel uses /etc/init.d/php-fpm-{83} or systemd unit "php-fpm-83"
            return 'php-fpm-'.$mmNoDot;
        }
        if (in_array($os, ['ubuntu', 'debian'], true) || $build === 'sury' || $build === 'debian-default') {
            return "php{$mm}-fpm";
        }
        if (in_array($os, ['rhel', 'centos', 'rocky', 'alma', 'fedora'], true)) {
            return 'php-fpm';
        }
        if ($os === 'alpine') {
            return "php-fpm{$mmNoDot}";
        }
        return 'php-fpm';
    }

    private function detectWebUser(string $os): string
    {
        // Common defaults
        $candidates = match (true) {
            in_array($os, ['ubuntu', 'debian'], true) => ['www-data'],
            in_array($os, ['rhel', 'centos', 'rocky', 'alma', 'fedora'], true) => ['nginx', 'apache'],
            $os === 'alpine' => ['nginx', 'www-data'],
            default => ['www-data', 'nginx', 'apache', 'www'],
        };
        // aaPanel often runs as "www"
        if (is_dir('/www/server')) {
            array_unshift($candidates, 'www');
        }

        // Find first existing
        foreach ($candidates as $user) {
            if ($this->userExists($user)) {
                return $user;
            }
        }
        return $candidates[0];
    }

    private function userExists(string $user): bool
    {
        if (function_exists('posix_getpwnam')) {
            return posix_getpwnam($user) !== false;
        }
        $passwd = @file_get_contents('/etc/passwd');
        if (! is_string($passwd)) {
            return false;
        }
        return (bool) preg_match('/^'.preg_quote($user, '/').':/m', $passwd);
    }

    private function canSudoNoPass(): bool
    {
        if (! $this->shellExecAvailable()) {
            return false;
        }
        // -n: non-interactive (will fail if password is required)
        $out = @shell_exec('sudo -n true 2>&1; echo "EXIT:$?"');
        if (! is_string($out)) {
            return false;
        }
        return (bool) preg_match('/EXIT:0\s*$/', $out);
    }

    private function commandExists(string $cmd): bool
    {
        // Defensive: don't rely solely on shell_exec
        $paths = explode(PATH_SEPARATOR, (string) (getenv('PATH') ?: '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'));
        foreach ($paths as $p) {
            $full = rtrim($p, '/').'/'.$cmd;
            if (is_file($full) && is_executable($full)) {
                return true;
            }
        }
        if ($this->shellExecAvailable()) {
            $out = @shell_exec('command -v '.escapeshellarg($cmd).' 2>/dev/null');
            return is_string($out) && trim($out) !== '';
        }
        return false;
    }

    private function shellExecAvailable(): bool
    {
        return function_exists('shell_exec') && ! $this->isFunctionDisabled('shell_exec');
    }

    private function isFunctionDisabled(string $fn): bool
    {
        $disabled = (string) ini_get('disable_functions');
        $list = array_map('trim', explode(',', $disabled));
        return in_array($fn, $list, true);
    }
}
