<?php
declare(strict_types=1);

/**
 * Auto-removes long-inactive accounts in three phases:
 *
 *  1. Identify users whose `last_login` is older than the configured threshold
 *     (default 12 months) and that have not yet received a warning. Send them a
 *     warning email pointing at the login URL and stamp `inactive_warning_sent_at`.
 *
 *  2. Identify users whose warning is older than the configured grace period
 *     (default 14 days) and whose `last_login` is still older than the
 *     threshold (i.e. they did not log in after the warning). Delete them.
 *
 *  3. A successful login resets `inactive_warning_sent_at = NULL` (handled in
 *     `Auth::login`). The next sweep treats the account as active again.
 *
 * Admins are excluded from both phases so the instance cannot lock itself out.
 *
 * The cron tick calls `runIfDue()`, which throttles to once per hour and skips
 * entirely when the feature is disabled in admin settings.
 */
class InactiveUserCleanup
{
    /** Minimum interval between sweeps, in seconds. */
    private const INTERVAL = 3600;

    public static function runIfDue(): void
    {
        if (Helper::getSetting('inactive_user_cleanup_enabled', '0') !== '1') return;
        $last = (int)Helper::getSetting('inactive_user_last_run', '0');
        if ($last !== 0 && (time() - $last) < self::INTERVAL) return;
        self::run();
    }

    public static function run(): array
    {
        $thresholdMonths = max(1, min(120, (int)Helper::getSetting('inactive_user_threshold_months', '12')));
        $graceDays       = max(1, min(90,  (int)Helper::getSetting('inactive_user_warning_days',     '14')));

        $thresholdCutoff = self::cutoffSqlDate($thresholdMonths . ' months');
        // Account counts as still-inactive if last_login is older than threshold.
        // Warning is "due to expire" when warning_sent_at is older than graceDays.
        $now = time();
        $graceCutoff = $now - $graceDays * 86400;

        $warnedCount  = self::sendWarnings($thresholdCutoff);
        $deletedCount = self::deleteExpired($thresholdCutoff, $graceCutoff);

        Helper::setSetting('inactive_user_last_run',     (string)$now);
        Helper::setSetting('inactive_user_last_warned',  (string)$warnedCount);
        Helper::setSetting('inactive_user_last_deleted', (string)$deletedCount);
        return ['warned' => $warnedCount, 'deleted' => $deletedCount];
    }

    /** Phase 1 — send "your account will be deleted in N days" mails. */
    private static function sendWarnings(string $thresholdCutoff): int
    {
        $db = Database::getInstance();
        $rows = $db->fetchAll(
            "SELECT id, username, email, language, timezone, last_login
             FROM users
             WHERE is_active = 1
               AND role != 'admin'
               AND auth_provider = 'local'
               AND COALESCE(last_login, created_at) < ?
               AND inactive_warning_sent_at IS NULL",
            [$thresholdCutoff]
        );
        $sent = 0;
        $graceDays = (int)Helper::getSetting('inactive_user_warning_days', '14');
        foreach ($rows as $u) {
            if (empty($u['email'])) continue;
            try {
                Mailer::sendInactiveAccountWarning($u, $graceDays);
            } catch (\Throwable $e) {
                error_log('[Plenvo] inactive-warning mail failed for user ' . $u['id'] . ': ' . $e->getMessage());
                continue;
            }
            $db->execute('UPDATE users SET inactive_warning_sent_at = ? WHERE id = ?', [time(), (int)$u['id']]);
            SecurityLog::log('inactive_user_warned', SecurityLog::SEV_WARN, [
                'email_hint'  => Helper::maskEmail((string)$u['email']),
                'grace_days'  => (string)$graceDays,
            ], (int)$u['id']);
            $sent++;
        }
        return $sent;
    }

    /** Phase 2 — delete accounts whose warning has fully expired. */
    private static function deleteExpired(string $thresholdCutoff, int $graceCutoff): int
    {
        $db = Database::getInstance();
        $rows = $db->fetchAll(
            "SELECT id, email FROM users
             WHERE is_active = 1
               AND role != 'admin'
               AND auth_provider = 'local'
               AND COALESCE(last_login, created_at) < ?
               AND inactive_warning_sent_at IS NOT NULL
               AND inactive_warning_sent_at < ?",
            [$thresholdCutoff, $graceCutoff]
        );
        $deleted = 0;
        foreach ($rows as $u) {
            $userId = (int)$u['id'];
            try {
                self::deleteUserAndSoleOwnedProjects($db, $userId);
            } catch (\Throwable $e) {
                error_log('[Plenvo] inactive-deletion failed for user ' . $userId . ': ' . $e->getMessage());
                continue;
            }
            SecurityLog::log('inactive_user_deleted', SecurityLog::SEV_CRITICAL, [
                'email_hint' => Helper::maskEmail((string)$u['email']),
            ], $userId);
            $deleted++;
        }
        return $deleted;
    }

    /**
     * Project handling: any project where the user is the sole `owner` in
     * `project_members` is deleted up-front. Then `SettingsController::deleteUser`
     * handles everything else (its own admin-fallback logic deals with
     * `projects.owner_id` reassignment for the remaining projects).
     */
    private static function deleteUserAndSoleOwnedProjects(Database $db, int $userId): void
    {
        // Defensive belt-and-braces: even if a future caller forgets the
        // auth_provider/role filter in the SELECT, refuse to delete LDAP-
        // managed accounts or admins here. The directory is the source of
        // truth for LDAP users and admins must never be auto-removed.
        $u = $db->fetch('SELECT auth_provider, role FROM users WHERE id = ?', [$userId]);
        if (!$u || ($u['auth_provider'] ?? 'local') !== 'local' || ($u['role'] ?? 'user') === 'admin') {
            return;
        }

        $soleOwnerProjects = $db->fetchAll(
            "SELECT pm.project_id
             FROM project_members pm
             WHERE pm.user_id = ? AND pm.role = 'owner'
               AND NOT EXISTS (
                 SELECT 1 FROM project_members pm2
                 WHERE pm2.project_id = pm.project_id
                   AND pm2.role = 'owner'
                   AND pm2.user_id != pm.user_id
               )",
            [$userId]
        );
        foreach ($soleOwnerProjects as $row) {
            $db->execute('DELETE FROM projects WHERE id = ?', [(int)$row['project_id']]);
        }
        SettingsController::deleteUser($db, $userId);
    }

    /**
     * Build a SQL DATETIME cutoff string ("now() minus offset"). SQLite's
     * datetime() supports "-12 months" / "-14 days" syntax.
     */
    private static function cutoffSqlDate(string $offset): string
    {
        $ts = strtotime('-' . $offset);
        return $ts !== false ? date('Y-m-d H:i:s', $ts) : date('Y-m-d H:i:s', time() - 365 * 86400);
    }
}
