<?php
/**
 * One-shot orphan-resource cleanup for a deleted WHP user.
 *
 * Why this exists
 * ---------------
 * Earlier versions of usermgmt::delete_user() removed only the system user
 * and the top-level MySQL account. They did NOT drop sub-users, owned
 * databases, sites, containers, HAProxy state, DNS zones, or per-user rows
 * across the whp.* tracking tables. Re-importing a user with the same name
 * (the cpanel-import flow) trips over the residue. The 2026-05-31 darkside
 * test on whp02 was the immediate trigger.
 *
 * Forward-fix lives in libs/usermgmt.php (calls UserResourceTeardown
 * before userdel). This script remediates servers where the bug already
 * fired.
 *
 * Usage
 * -----
 *   php cleanup-orphan-user-resources.php --username NAME [--apply]
 *
 *   --username NAME   Required. The deleted panel username to clean up.
 *   --apply           Default = dry-run. Pass --apply to actually delete.
 *   --log PATH        Override log path. Default
 *                     /var/log/whp/cleanup-orphan-user-<name>-<UTC-ts>.log
 *
 * Safety
 * ------
 *   - REFUSES to run if `getent passwd <name>` returns a row. Only
 *     orphans (users no longer on the system) are valid targets.
 *   - REFUSES if <name> is a protected system user
 *     (root, mysql, postgres, etc — see usermgmt::$protected_users).
 *   - Dry-run is the default. --apply must be passed explicitly.
 *   - Idempotent: re-running on an already-clean user is a no-op.
 */

require_once('/docker/whp/web/configs/config.php');
require_once('/docker/whp/web/libs/mysqlmgmt.php');
require_once('/docker/whp/web/libs/usermgmt.php');
require_once('/docker/whp/web/libs/user-resource-teardown.php');

// -- arg parsing --------------------------------------------------------

$opts = getopt('', ['username:', 'apply', 'log:', 'help']);
if (isset($opts['help']) || empty($opts['username'])) {
    fwrite(STDERR,
        "Usage: php cleanup-orphan-user-resources.php --username NAME [--apply] [--log PATH]\n" .
        "       --username   Deleted panel username to clean up (required)\n" .
        "       --apply      Actually perform the teardown (default = dry-run)\n" .
        "       --log PATH   Override log file path\n"
    );
    exit(1);
}
$username = (string)$opts['username'];
$apply    = isset($opts['apply']);

// -- safety gates -------------------------------------------------------

// 1. Username must pass the same regex as the panel uses.
if (!preg_match('/^[a-z][a-z0-9_-]{0,31}$/', $username)
    || str_contains($username, '..') || str_contains($username, '/')) {
    fwrite(STDERR, "Invalid username: $username\n");
    exit(2);
}

// 2. Not a protected system user.
$um = new usermgmt();
$reflect = new ReflectionClass($um);
$method  = $reflect->getMethod('is_protected_user');
$method->setAccessible(true);
if ($method->invoke($um, $username)) {
    fwrite(STDERR, "Refusing to clean up protected user: $username\n");
    exit(2);
}

// 3. UNIX user must NOT exist. We only ever operate on orphans.
$idOut = trim((string)shell_exec('/usr/bin/getent passwd ' . escapeshellarg($username) . ' 2>/dev/null'));
if ($idOut !== '') {
    fwrite(STDERR,
        "Refusing to clean up: UNIX user '$username' still exists in /etc/passwd.\n" .
        "This tool only sweeps orphans left behind by an earlier delete.\n" .
        "If you need to delete this user, do it through the panel (which calls\n" .
        "UserResourceTeardown automatically).\n"
    );
    exit(3);
}

// -- log setup ----------------------------------------------------------

$logPath = $opts['log']
    ?? '/var/log/whp/cleanup-orphan-user-' . $username . '-' . gmdate('Ymd-His') . '.log';
@mkdir(dirname($logPath), 0750, true);
$logFh = fopen($logPath, 'a');
if (!$logFh) {
    fwrite(STDERR, "Cannot open log file: $logPath\n");
    exit(4);
}
$log = function ($msg) use ($logFh) {
    $line = '[' . gmdate('Y-m-d H:i:s') . 'Z] ' . $msg;
    fwrite($logFh, $line . "\n");
    echo $line . "\n";
};

$log("=== orphan-user-resource cleanup ===");
$log("username: $username");
$log("mode:     " . ($apply ? 'APPLY (destructive)' : 'DRY-RUN (no changes)'));
$log("log:      $logPath");

// -- mysql connection ---------------------------------------------------

$pdo = (new mysqlmgmt())->getMySQLConnection();
if (!$pdo) {
    $log("ERROR: cannot open MySQL connection");
    exit(5);
}

// -- dry-run probe ------------------------------------------------------

$probe = function ($label, $sql, $params) use ($pdo, $log) {
    try {
        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        $rows = $stmt->fetchAll(PDO::FETCH_COLUMN);
        $log(sprintf("%-40s %5d row(s)%s",
            $label,
            count($rows),
            count($rows) && count($rows) <= 50
                ? '  → ' . implode(', ', array_slice($rows, 0, 50))
                : ''
        ));
        return $rows;
    } catch (Throwable $e) {
        // Schema drift on older deployments — skip silently.
        if (str_contains($e->getMessage(), "doesn't exist")
            || str_contains($e->getMessage(), 'Unknown column')) {
            return [];
        }
        $log("  probe '$label' error: " . $e->getMessage());
        return [];
    }
};

$log("--- pre-teardown inventory ---");
$probe('sites',           "SELECT site_name FROM whp.sites WHERE username = ?", [$username]);
$probe('domains',         "SELECT domain_name FROM whp.domains WHERE username = ?", [$username]);
$probe('user_databases',  "SELECT database_name FROM whp.user_databases WHERE owner_username = ?", [$username]);
$probe('cpanel_imports',  "SELECT id FROM whp.cpanel_imports WHERE username = ?", [$username]);
$probe('haproxy_domains', "SELECT domain_name FROM whp.haproxy_domains
                            WHERE domain_name IN (SELECT domain_name FROM whp.domains WHERE username = ?)
                               OR domain_name IN (SELECT sd.fqdn FROM whp.site_domains sd
                                                    JOIN whp.domains d ON sd.domain_id = d.id
                                                   WHERE d.username = ?)", [$username, $username]);
$probe('mysql.user sub',  "SELECT CONCAT(User,'@',Host) FROM mysql.user
                            WHERE User LIKE CONCAT(REPLACE(?, '_', '\\_'), '\\_%')", [$username]);
$probe('mysql databases', "SELECT SCHEMA_NAME FROM information_schema.SCHEMATA
                            WHERE SCHEMA_NAME LIKE CONCAT(REPLACE(?, '_', '\\_'), '\\_%')", [$username]);

if (!$apply) {
    $log("--- DRY-RUN complete. Re-run with --apply to perform teardown. ---");
    exit(0);
}

// -- destructive phase --------------------------------------------------

$log("--- running UserResourceTeardown::tearDown ---");
$teardown = new UserResourceTeardown($pdo, function ($msg) use ($log) {
    $log("  teardown: $msg");
});

$report = $teardown->tearDown($username);

$log("--- report ---");
$log("sites:        " . count($report['sites']));
foreach ($report['sites'] as $s) {
    $log(sprintf("  site %d: removed=%s containers=%d errors=%s",
        $s['site_id'],
        !empty($s['removed']) ? 'yes' : 'no',
        $s['containers_removed'] ?? 0,
        $s['errors'] ? implode('; ', $s['errors']) : 'none'
    ));
}
$log("databases:    " . count($report['databases']) . ($report['databases'] ? ' → ' . implode(', ', $report['databases']) : ''));
$log("db_users:     " . count($report['db_users']) . ($report['db_users'] ? ' → ' . implode(', ', $report['db_users']) : ''));
$log("pg_user:      " . ($report['pg_user'] ?? '(none)'));
$log("pg_databases: " . count($report['pg_databases']) . ($report['pg_databases'] ? ' → ' . implode(', ', $report['pg_databases']) : ''));
$log("rows deleted by table:");
foreach ($report['rows'] as $table => $count) {
    if ($count > 0) {
        $log(sprintf("  %-30s %d", $table, $count));
    }
}
if ($report['errors']) {
    $log("errors:");
    foreach ($report['errors'] as $err) {
        $log("  - $err");
    }
} else {
    $log("errors: none");
}

$log("--- done ---");
fclose($logFh);
exit($report['errors'] ? 6 : 0);
