<?php

namespace App\Support\Os;

/**
 * Builds SHELL COMMAND STRINGS for package operations, per manager. It never runs
 * anything — FleetService is the single executor (base64-wrapped, sudo + LC_ALL=C
 * handled there). Only apt/dnf/zypper are driven this release; package names passed
 * in are Clusev constants (fail2ban, ufw, …), never operator input.
 *
 * Note: the dnf/zypper command strings are constructed from documented syntax but
 * are exercised live only on the families a tester actually runs; the apt path is
 * the one verified against the live fleet today.
 */
final class PackageManager
{
    /** @param  'apt'|'dnf'|'zypper'|'none'  $manager */
    public function __construct(
        private string $manager,
        private bool $useYum = false,   // dnf family lacking the dnf binary -> yum
    ) {}

    public static function for(OsProfile $os): self
    {
        $useYum = $os->packageManager === 'dnf' && ! $os->has('dnf') && $os->has('yum');

        return new self($os->packageManager, $useYum);
    }

    private function dnf(): string
    {
        return $this->useYum ? 'yum' : 'dnf';
    }

    /**
     * A command that prints `CLUSEV_OK` (only when the query actually ran) and
     * `CLUSEV_PENDING=<n>`. Runs unprivileged (simulation / metadata read only).
     */
    public function pendingScript(): string
    {
        return match ($this->manager) {
            'apt' => 'out=$(apt-get -s upgrade 2>/dev/null) && echo CLUSEV_OK; '
                ."echo \"CLUSEV_PENDING=\$(printf '%s\\n' \"\$out\" | grep -c '^Inst ')\"",
            'dnf' => 'out=$('.$this->dnf().' -q check-update 2>/dev/null); rc=$?; '
                .'if [ "$rc" = 0 ] || [ "$rc" = 100 ]; then echo CLUSEV_OK; fi; '
                ."echo \"CLUSEV_PENDING=\$(printf '%s\\n' \"\$out\" | grep -cE '^[[:graph:]]+[[:space:]]+[[:graph:]]+[[:space:]]+[[:graph:]]+')\"",
            'zypper' => 'out=$(zypper --non-interactive list-updates 2>/dev/null); rc=$?; '
                .'[ "$rc" = 0 ] && echo CLUSEV_OK; '
                ."echo \"CLUSEV_PENDING=\$(printf '%s\\n' \"\$out\" | grep -cE '^v \\|')\"",
            default => 'echo CLUSEV_PENDING=0',
        };
    }

    /**
     * Like pendingScript, but ALSO emits `CLUSEV_SECURITY=<m>` — the subset of pending upgrades that
     * come from a security source. One round-trip for both counts. Best-effort per family (the apt
     * path is solid; dnf/zypper security queries degrade to 0 when the metadata isn't present).
     */
    public function countsScript(): string
    {
        return match ($this->manager) {
            'apt' => 'out=$(apt-get -s upgrade 2>/dev/null) && echo CLUSEV_OK; '
                ."echo \"CLUSEV_PENDING=\$(printf '%s\\n' \"\$out\" | grep -c '^Inst ')\"; "
                ."echo \"CLUSEV_SECURITY=\$(printf '%s\\n' \"\$out\" | grep '^Inst ' | grep -ic 'securit')\"",
            'dnf' => 'out=$('.$this->dnf().' -q check-update 2>/dev/null); rc=$?; '
                .'if [ "$rc" = 0 ] || [ "$rc" = 100 ]; then echo CLUSEV_OK; fi; '
                ."echo \"CLUSEV_PENDING=\$(printf '%s\\n' \"\$out\" | grep -cE '^[[:graph:]]+[[:space:]]+[[:graph:]]+[[:space:]]+[[:graph:]]+')\"; "
                .'echo "CLUSEV_SECURITY=$('.$this->dnf().' -q --security check-update 2>/dev/null | grep -cE \'^[[:graph:]]+[[:space:]]+[[:graph:]]+[[:space:]]+[[:graph:]]+\')"',
            'zypper' => 'out=$(zypper --non-interactive list-updates 2>/dev/null); rc=$?; '
                .'[ "$rc" = 0 ] && echo CLUSEV_OK; '
                ."echo \"CLUSEV_PENDING=\$(printf '%s\\n' \"\$out\" | grep -cE '^v \\|')\"; "
                .'echo "CLUSEV_SECURITY=$(zypper --non-interactive list-patches --category security 2>/dev/null | grep -cE \'^[[:alnum:]]\')"',
            default => 'echo CLUSEV_PENDING=0; echo CLUSEV_SECURITY=0',
        };
    }

    /** Refresh index + apply all upgrades (privileged, long-running). */
    public function applyScript(): string
    {
        return match ($this->manager) {
            'apt' => 'apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y upgrade',
            'dnf' => $this->dnf().' -y upgrade',
            // zypper exits 102 (reboot required) / 103 (restart zypper) on SUCCESS —
            // normalize those to 0 so a successful upgrade is not reported as failed.
            'zypper' => 'zypper --non-interactive refresh && { zypper --non-interactive update; rc=$?; '
                .'[ "$rc" = 0 ] || [ "$rc" = 102 ] || [ "$rc" = 103 ]; }',
            default => 'false',
        };
    }

    /** Install one package (privileged). */
    public function installScript(string $pkg): string
    {
        return match ($this->manager) {
            'apt' => 'DEBIAN_FRONTEND=noninteractive apt-get install -y '.$pkg,
            'dnf' => $this->dnf().' install -y '.$pkg,
            'zypper' => 'zypper --non-interactive install '.$pkg,
            default => 'false',
        };
    }

    /** A shell CONDITION that exits 0 when $pkg is installed (for use with && / ||). */
    public function isInstalledTest(string $pkg): string
    {
        return match ($this->manager) {
            'apt' => "dpkg -l {$pkg} 2>/dev/null | grep -q '^ii'",
            'dnf', 'zypper' => "rpm -q {$pkg} >/dev/null 2>&1",
            default => 'false',
        };
    }

    /** Install $pkg only if missing (idempotent). */
    public function ensureInstalledScript(string $pkg): string
    {
        return '('.$this->isInstalledTest($pkg).' || '.$this->installScript($pkg).')';
    }
}
