<?php

namespace App\Support\Os;

use App\Models\Server;
use App\Services\FleetService;
use Illuminate\Support\Facades\Cache;

/**
 * Resolves a Server's OsProfile from one unprivileged probe (/etc/os-release +
 * `command -v`), cached for an hour per server. The family is inferred from
 * ID/ID_LIKE, cross-checked against the binaries actually present so a derivative
 * (e.g. ID_LIKE="rhel fedora") still resolves and the installed tool wins.
 */
class OsDetector
{
    private const PROBE_BINARIES = [
        'apt-get', 'dnf', 'yum', 'zypper', 'pacman', 'apk',
        'firewall-cmd', 'ufw', 'nft', 'systemctl', 'rc-service',
    ];

    public function __construct(private FleetService $fleet) {}

    /** Detect (and cache) the OS profile of a server. */
    public function detect(Server $server, bool $fresh = false): OsProfile
    {
        $key = 'os-profile:'.$server->id;

        if (! $fresh) {
            $cached = Cache::get($key);
            if ($cached instanceof OsProfile) {
                return $cached;
            }
        }

        // Include the sbin dirs: ufw/firewall-cmd live in /usr/sbin, which is not on a
        // non-root user's PATH, so a bare `command -v ufw` would miss an installed tool.
        // Guard the source: `.` is a POSIX special built-in, so sourcing a MISSING
        // /etc/os-release would make a non-interactive sh exit before the binary
        // probe runs — defeating the package-manager fallback. Only source when readable.
        $probe = 'export PATH="/usr/local/sbin:/usr/sbin:/sbin:$PATH"; '
            .'[ -r /etc/os-release ] && . /etc/os-release 2>/dev/null; '
            .'echo "ID=${ID:-}"; echo "ID_LIKE=${ID_LIKE:-}"; echo "PRETTY=${PRETTY_NAME:-}"; '
            .'for p in '.implode(' ', self::PROBE_BINARIES).'; do command -v "$p" >/dev/null 2>&1 && echo "HAS=$p"; done; '
            // which firewall is actually RUNNING — decides the tool when both are installed.
            .'systemctl is-active --quiet firewalld 2>/dev/null && echo ACTIVE=firewalld; '
            .'systemctl is-active --quiet ufw 2>/dev/null && echo ACTIVE=ufw';

        // Parse the output regardless of exit code: the probe's status reflects the
        // LAST `command -v` (often a binary that is legitimately absent, e.g.
        // rc-service on a systemd host), so a non-zero exit is normal and the
        // ID=/HAS= lines that printed are still valid.
        $res = $this->fleet->runPlain($server, $probe);
        $profile = $this->parse($res['output']);

        // Don't lock in a "nothing detected" result (transient SSH failure / host
        // down) for a full hour — re-probe soon in that case.
        $detected = $profile->id !== '' || $profile->available !== [];
        Cache::put($key, $profile, $detected ? now()->addHour() : now()->addSeconds(60));

        return $profile;
    }

    /** Drop the cached profile (call after a credential change or on demand). */
    public function forget(Server $server): void
    {
        Cache::forget('os-profile:'.$server->id);
    }

    private function parse(string $out): OsProfile
    {
        $id = '';
        $idLike = '';
        $pretty = '';
        $available = [];
        $active = [];

        foreach (preg_split('/\R/', $out) ?: [] as $line) {
            $line = trim($line);
            if (str_starts_with($line, 'ID=')) {
                $id = strtolower(trim(substr($line, 3), " \"'"));
            } elseif (str_starts_with($line, 'ID_LIKE=')) {
                $idLike = strtolower(trim(substr($line, 8), " \"'"));
            } elseif (str_starts_with($line, 'PRETTY=')) {
                $pretty = trim(substr($line, 7), " \"'");
            } elseif (str_starts_with($line, 'HAS=')) {
                $available[] = substr($line, 4);
            } elseif (str_starts_with($line, 'ACTIVE=')) {
                $active[] = substr($line, 7);
            }
        }

        $tokens = array_values(array_filter(array_merge([$id], preg_split('/\s+/', $idLike) ?: [])));
        $family = $this->family($tokens, $available);

        return new OsProfile(
            family: $family,
            id: $id,
            prettyName: $pretty !== '' ? $pretty : 'Unbekanntes System',
            packageManager: $this->packageManager($available),
            firewallTool: $this->firewallTool($family, $available, $active),
            serviceManager: in_array('systemctl', $available, true)
                ? 'systemd'
                : (in_array('rc-service', $available, true) ? 'openrc' : 'none'),
            available: $available,
        );
    }

    /** @param  list<string>  $tokens */
    private function family(array $tokens, array $available): string
    {
        $hit = fn (array $names): bool => count(array_intersect($names, $tokens)) > 0;

        if ($hit(['debian', 'ubuntu', 'raspbian', 'linuxmint', 'pop', 'devuan', 'kali'])) {
            return 'debian';
        }
        if ($hit(['rhel', 'fedora', 'centos', 'rocky', 'almalinux', 'ol', 'amzn'])) {
            return 'rhel';
        }
        if ($hit(['suse', 'opensuse', 'opensuse-leap', 'opensuse-tumbleweed', 'sles', 'sled'])) {
            return 'suse';
        }
        if ($hit(['arch', 'manjaro', 'endeavouros', 'arcolinux'])) {
            return 'arch';
        }
        if ($hit(['alpine'])) {
            return 'alpine';
        }

        // No ID match — fall back to whichever package manager is actually present.
        return match (true) {
            in_array('apt-get', $available, true) => 'debian',
            in_array('dnf', $available, true), in_array('yum', $available, true) => 'rhel',
            in_array('zypper', $available, true) => 'suse',
            in_array('pacman', $available, true) => 'arch',
            in_array('apk', $available, true) => 'alpine',
            default => 'unknown',
        };
    }

    /** @param  list<string>  $available */
    private function packageManager(array $available): string
    {
        return match (true) {
            in_array('apt-get', $available, true) => 'apt',
            in_array('dnf', $available, true), in_array('yum', $available, true) => 'dnf',
            in_array('zypper', $available, true) => 'zypper',
            in_array('pacman', $available, true) => 'pacman',
            in_array('apk', $available, true) => 'apk',
            default => 'none',
        };
    }

    /**
     * @param  list<string>  $available
     * @param  list<string>  $active  firewall tools reported running (firewalld/ufw)
     */
    private function firewallTool(string $family, array $available, array $active): string
    {
        $hasFirewalld = in_array('firewall-cmd', $available, true);
        $hasUfw = in_array('ufw', $available, true);

        // Both installed: the RUNNING one wins (managing the inactive tool would
        // report the wrong state and could start a second firewall manager).
        if ($hasFirewalld && $hasUfw) {
            if (in_array('firewalld', $active, true)) {
                return 'firewalld';
            }
            if (in_array('ufw', $active, true)) {
                return 'ufw';
            }
        } elseif ($hasFirewalld) {
            return 'firewalld';
        } elseif ($hasUfw) {
            return 'ufw';
        }

        // Nothing installed (or both present but neither active) — default by family
        // so enable() installs/uses the conventional tool for the distro.
        return match ($family) {
            'rhel', 'suse' => 'firewalld',
            'debian', 'arch', 'alpine' => 'ufw',
            default => 'none',
        };
    }
}
