<?php

namespace App\Services;

use App\Models\Server;
use App\Support\Os\OsDetector;
use App\Support\Os\PackageManager;

/**
 * Package-maintenance helpers across apt/dnf/zypper (resolved per OS via
 * OsDetector): counts pending package updates and applies an upgrade.
 *
 * Every reader/writer goes through FleetService, so quoting is base64-safe and
 * privileged work runs as root. (fail2ban tuning + management lives in Fail2banService.)
 */
class MaintenanceService
{
    public function __construct(private FleetService $fleet, private OsDetector $detector) {}

    /**
     * NULL when Clusev can manage package updates on this host, otherwise a German
     * reason (e.g. unsupported package manager) for the operator.
     */
    public function updateSupport(Server $server): ?string
    {
        return $this->detector->detect($server)->supports('updates');
    }

    /**
     * Number of pending package upgrades via a dry-run/metadata read (no root
     * needed), or NULL when it could not run (unsupported manager / errored) — so
     * the caller shows "unbekannt" rather than a misleading "0 / up to date". The
     * remote script emits a CLUSEV_OK sentinel only when the query actually ran.
     */
    public function pendingUpdates(Server $server): ?int
    {
        $os = $this->detector->detect($server);
        if (! $os->managesPackages()) {
            return null;
        }

        $res = $this->fleet->runPlain($server, PackageManager::for($os)->pendingScript());
        if (! str_contains($res['output'], 'CLUSEV_OK')) {
            return null;
        }

        return preg_match('/CLUSEV_PENDING=(\d+)/', $res['output'], $m) ? (int) $m[1] : null;
    }

    /**
     * Pending update count AND its security subset in one round-trip. Either value is NULL when the
     * query couldn't run (unsupported manager / errored) so the caller shows "unbekannt" rather than
     * a misleading 0. Security is best-effort per family; it is always ≤ pending.
     *
     * @return array{pending: ?int, security: ?int}
     */
    public function updateCounts(Server $server): array
    {
        $os = $this->detector->detect($server);
        if (! $os->managesPackages()) {
            return ['pending' => null, 'security' => null];
        }

        $res = $this->fleet->runPlain($server, PackageManager::for($os)->countsScript());
        if (! str_contains($res['output'], 'CLUSEV_OK')) {
            return ['pending' => null, 'security' => null];
        }

        $pending = preg_match('/CLUSEV_PENDING=(\d+)/', $res['output'], $m) ? (int) $m[1] : null;
        $security = preg_match('/CLUSEV_SECURITY=(\d+)/', $res['output'], $s) ? (int) $s[1] : null;

        // Security can never exceed total pending (guards a noisy grep).
        if ($pending !== null && $security !== null) {
            $security = min($security, $pending);
        }

        return ['pending' => $pending, 'security' => $security];
    }

    /**
     * Refresh the index and apply all upgrades as root, using the host's package
     * manager. Long-running, so a 900s timeout. Output trimmed to the last ~400
     * chars for a compact result panel.
     *
     * @return array{ok: bool, output: string}
     */
    public function applyUpgrades(Server $server): array
    {
        $os = $this->detector->detect($server);
        if ($reason = $os->supports('updates')) {
            return ['ok' => false, 'output' => $reason];
        }

        $res = $this->fleet->runPrivileged($server, PackageManager::for($os)->applyScript(), 900);

        return ['ok' => $res['ok'], 'output' => $this->tail($res['output'], 400)];
    }

    /** Keep only the last $max characters of a (multi-line) output. */
    private function tail(string $out, int $max): string
    {
        $out = trim($out);
        if (mb_strlen($out) <= $max) {
            return $out;
        }

        return '…'.mb_substr($out, -$max);
    }
}
