<?php
/**
 * Disk-usage snapshot collector. Surveys the host filesystem in three
 * dimensions (users, services, Docker) plus a cleanup-candidates probe
 * and a top-level df. The result is JSON-serializable and gets cached in
 * whp.disk_usage_snapshot for the Server Admin > Disk Usage page.
 *
 * Runs from cron as root every 6 hours, or on-demand from the page's
 * Refresh button. Bounded execution time (~30s on a busy host) -- the
 * `du` calls dominate; we cap each scan with a one-level-deep bound and
 * a 25s timeout per command to keep a runaway directory from stalling
 * the snapshot.
 */

if (!function_exists('whp_du_collect_snapshot')) {

/** Run a shell command bounded to $timeout seconds. Returns stdout or ''. */
function whp_du_run(string $cmd, int $timeout = 25): string {
    $full = "timeout {$timeout}s sh -c " . escapeshellarg($cmd) . " 2>/dev/null";
    $out = shell_exec($full);
    return is_string($out) ? trim($out) : '';
}

/** du -sb on a path. Returns bytes (0 if not present / unreadable). */
function whp_du_bytes(string $path): int {
    if (!file_exists($path)) return 0;
    $cmd = "du -sb " . escapeshellarg($path) . " 2>/dev/null | cut -f1";
    return (int) whp_du_run($cmd, 30);
}

/** Per-user totals for /docker/users/<u>. Sorted largest first. */
function whp_du_user_breakdown(): array {
    $rows = [];
    foreach (glob('/docker/users/*', GLOB_ONLYDIR) ?: [] as $dir) {
        $u = basename($dir);
        if ($u === '' || $u[0] === '.') continue;
        $bytes = whp_du_bytes($dir);
        // Drill one level deeper: per-domain inside the user.
        $domains = [];
        foreach (glob($dir . '/*', GLOB_ONLYDIR) ?: [] as $sub) {
            $b = whp_du_bytes($sub);
            if ($b > 0) {
                $domains[] = ['name' => basename($sub), 'bytes' => $b];
            }
        }
        usort($domains, fn($a, $b) => $b['bytes'] <=> $a['bytes']);
        $rows[] = ['username' => $u, 'bytes' => $bytes, 'breakdown' => $domains];
    }
    usort($rows, fn($a, $b) => $b['bytes'] <=> $a['bytes']);
    return $rows;
}

/**
 * Service-level breakdown of system + WHP-managed paths. Each entry has
 * an optional `cleanup_hint` describing whether it's safe to clear/truncate.
 */
function whp_du_services_breakdown(): array {
    // path => [label, cleanup_hint]. Cleanup hints map to the patterns
    // accepted by /api/disk-cleanup.php so the UI can offer one-click
    // truncate/delete on rows that are safe to touch.
    $paths = [
        '/var/log/httpd'                 => ['Apache logs',          'rotated_logs'],
        '/var/log/php-fpm'               => ['PHP-FPM logs',         'rotated_logs'],
        '/var/log/proftpd'               => ['ProFTPD logs',         'rotated_logs'],
        '/var/log/whp-ai-monitor.log'    => ['AI monitor log',       'truncate_active'],
        '/var/log/whp-ai-monitor-daily.log' => ['AI monitor daily log', 'truncate_active'],
        '/var/log/whp-backup.log'        => ['Backup daemon log',    'truncate_active'],
        '/var/log/whp-monitor-poll.log'  => ['Health poll log',      'truncate_active'],
        '/var/log/wordpress-auto-update.log' => ['WordPress updater log', 'truncate_active'],
        '/var/log/haproxy-watchdog.log'  => ['HAProxy watchdog log', 'truncate_active'],
        '/var/log/haproxy-crash-monitor.log' => ['HAProxy crash monitor', 'truncate_active'],
        '/var/log/haproxy-crash-events.json' => ['HAProxy crash events JSONL', 'truncate_active'],
        '/var/log/whp-log-monitor.log'   => ['WHP log monitor',      'truncate_active'],
        '/docker/whp/web'                => ['WHP control panel files', null],
        '/docker/whp/backups'            => ['WHP local backups',    null],
        '/docker/whp/sql'                => ['WHP SQLite data',      null],
        '/docker/mysql'                  => ['MySQL/MariaDB datadir', null],
        '/docker/postgresql'             => ['PostgreSQL datadir',   null],
        '/docker/docker-data'            => ['Docker data root',     null],
        '/var/lib/whp-ai-monitor'        => ['AI monitor SQLite + reports', null],
        '/tmp/whp-backups'               => ['Backup staging tmp',   'safe_clear'],
        '/docker/tmp'                    => ['WHP scratch tmp',      'safe_clear'],
    ];
    $rows = [];
    foreach ($paths as $path => $meta) {
        if (!file_exists($path)) continue;
        $rows[] = [
            'path'  => $path,
            'label' => $meta[0],
            'bytes' => whp_du_bytes($path),
            'cleanup_hint' => $meta[1],
        ];
    }
    // journalctl --disk-usage is a one-line summary; fold in for visibility.
    $j = whp_du_run("journalctl --disk-usage 2>/dev/null | grep -oE '[0-9.]+[KMGT]?' | tail -1");
    if ($j !== '') {
        $rows[] = [
            'path'  => 'journalctl',
            'label' => 'systemd journal',
            'bytes' => whp_du_journal_to_bytes($j),
            'cleanup_hint' => 'journal_vacuum',
        ];
    }
    usort($rows, fn($a, $b) => $b['bytes'] <=> $a['bytes']);
    return $rows;
}

function whp_du_journal_to_bytes(string $human): int {
    // "184.0M", "1.2G" -> bytes
    if (!preg_match('/^([0-9.]+)([KMGT]?)$/', $human, $m)) return 0;
    $n = (float)$m[1];
    $suffix = $m[2];
    $mult = ['K' => 1024, 'M' => 1024**2, 'G' => 1024**3, 'T' => 1024**4][$suffix] ?? 1;
    return (int)($n * $mult);
}

/** Docker images, containers, volumes, and the system df overview. */
function whp_du_docker_breakdown(): array {
    $df = whp_du_run("docker system df --format '{{.Type}}|{{.TotalCount}}|{{.Active}}|{{.Size}}|{{.Reclaimable}}'", 15);
    $df_rows = [];
    foreach (preg_split('/\r?\n/', $df) ?: [] as $line) {
        if ($line === '') continue;
        $cols = explode('|', $line);
        if (count($cols) < 5) continue;
        $df_rows[] = [
            'type'        => $cols[0],
            'total'       => (int)$cols[1],
            'active'      => (int)$cols[2],
            'size'        => $cols[3],
            'reclaimable' => $cols[4],
        ];
    }

    $images = whp_du_run("docker images --no-trunc --format '{{.Repository}}|{{.Tag}}|{{.ID}}|{{.Size}}|{{.CreatedSince}}'", 15);
    $img_rows = [];
    foreach (preg_split('/\r?\n/', $images) ?: [] as $line) {
        if ($line === '') continue;
        $cols = explode('|', $line, 5);
        if (count($cols) < 5) continue;
        $img_rows[] = [
            'repository' => $cols[0],
            'tag'        => $cols[1],
            'id'         => $cols[2],
            'size'       => $cols[3],
            'created'    => $cols[4],
        ];
    }

    $containers = whp_du_run("docker ps -a --no-trunc --size --format '{{.Names}}|{{.Status}}|{{.Size}}|{{.Image}}'", 15);
    $ctr_rows = [];
    foreach (preg_split('/\r?\n/', $containers) ?: [] as $line) {
        if ($line === '') continue;
        $cols = explode('|', $line, 4);
        if (count($cols) < 4) continue;
        $ctr_rows[] = [
            'name'   => $cols[0],
            'status' => $cols[1],
            'size'   => $cols[2],
            'image'  => $cols[3],
        ];
    }

    return [
        'system_df'  => $df_rows,
        'images'     => $img_rows,
        'containers' => $ctr_rows,
    ];
}

/** Top-level df -h for every mounted local filesystem.
 * Excludes pseudo and per-container overlay mounts -- on a busy WHP host
 * Docker creates one `overlay` mount per running container and they all
 * report the same size/used/free as the underlying root filesystem, so
 * they're pure noise in this strip. The Docker tab shows per-container
 * sizes separately. */
function whp_du_filesystems(): array {
    $out = whp_du_run("df -h --output=source,target,fstype,size,used,avail,pcent --local -x tmpfs -x devtmpfs -x squashfs -x overlay -x fuse.lxcfs 2>/dev/null", 10);
    $rows = [];
    $lines = preg_split('/\r?\n/', $out) ?: [];
    foreach ($lines as $i => $line) {
        if ($i === 0 || trim($line) === '') continue; // header
        $cols = preg_split('/\s+/', trim($line));
        if (count($cols) < 7) continue;
        $rows[] = [
            'source'   => $cols[0],
            'target'   => $cols[1],
            'fstype'   => $cols[2],
            'size'     => $cols[3],
            'used'     => $cols[4],
            'avail'    => $cols[5],
            'pcent'    => $cols[6],
        ];
    }
    return $rows;
}

/** Wrap detect_cleanup_candidates() from libs/disk_cleanup_helpers.php.
 * We deliberately load the lib (pure functions, no auth, no dispatch)
 * rather than api/disk-cleanup.php -- the API file emits headers and
 * exits on missing AUTH_USER, which would kill the snapshot when run
 * from CLI cron. */
function whp_du_cleanup_candidates(): array {
    $lib = __DIR__ . '/disk_cleanup_helpers.php';
    if (!file_exists($lib)) return [];
    require_once $lib;
    if (function_exists('detect_cleanup_candidates')) {
        return detect_cleanup_candidates();
    }
    return [];
}

/** Take the snapshot. Returns the assembled payload array. */
function whp_du_collect_snapshot(): array {
    return [
        'collected_at'        => date('c'),
        'host'                => gethostname() ?: 'unknown',
        'filesystems'         => whp_du_filesystems(),
        'users'               => whp_du_user_breakdown(),
        'services'            => whp_du_services_breakdown(),
        'docker'              => whp_du_docker_breakdown(),
        'cleanup_candidates'  => whp_du_cleanup_candidates(),
    ];
}

/** Persist a freshly-taken snapshot to whp.disk_usage_snapshot. */
function whp_du_save_snapshot(PDO $db, array $payload, int $duration_ms): void {
    $json = json_encode($payload, JSON_UNESCAPED_SLASHES);
    if ($json === false) {
        throw new RuntimeException('snapshot json_encode failed: ' . json_last_error_msg());
    }
    $stmt = $db->prepare("
        INSERT INTO whp.disk_usage_snapshot (id, snapshot_at, duration_ms, payload)
        VALUES (1, NOW(), ?, ?)
        ON DUPLICATE KEY UPDATE
            snapshot_at = NOW(),
            duration_ms = VALUES(duration_ms),
            payload     = VALUES(payload)
    ");
    $stmt->execute([$duration_ms, $json]);
}

/** Read the latest snapshot. Returns null when none exists yet. */
function whp_du_load_snapshot(PDO $db): ?array {
    $stmt = $db->query("SELECT snapshot_at, duration_ms, payload FROM whp.disk_usage_snapshot WHERE id = 1");
    $row = $stmt->fetch(PDO::FETCH_ASSOC);
    if (!$row) return null;
    $payload = json_decode($row['payload'], true);
    if (!is_array($payload)) return null;
    return [
        'snapshot_at' => $row['snapshot_at'],
        'duration_ms' => (int)$row['duration_ms'],
        'payload'     => $payload,
    ];
}

}  // function_exists guard
