<?php

declare(strict_types=1);

/*
 * This file is part of the Ferienpass package.
 *
 * (c) Richard Henkenjohann <richard@ferienpass.online>
 *
 * For more information visit the project website <https://ferienpass.online>
 * or the documentation under <https://docs.ferienpass.online>.
 */

namespace Ferienpass\CoreBundle\Cron;

use Contao\CoreBundle\Cron\Cron;
use Contao\CoreBundle\DependencyInjection\Attribute\AsCronJob;
use Ferienpass\CoreBundle\Repository\UserRepository;

/**
 * Drops abandoned registrations: accounts that were created disabled and never
 * activated, so nothing but a stale row and an unread confirmation mail exists.
 *
 * "Never activated" is read from {@see \Ferienpass\CoreBundle\Entity\User::$activatedAt},
 * which every path that enables an account stamps. It used to be inferred from
 * the *absence* of an AccountActivated entry in the notification log — a marker
 * that only ever gets written when that mail is actually sent, so it is missing
 * for every account an admin activated by hand and on every instance where the
 * "Konto aktiviert" notification is disabled. Suspending such an account (a
 * routine admin action) therefore handed it to this cron, taking its
 * participants and attendances with it.
 *
 * $activatedAt is reliable for everything created from that change on, but it
 * cannot be reconstructed for older rows that were activated without leaving a
 * log ({@see \Ferienpass\CoreBundle\Migration\UserActivatedAtMigration} backfills
 * what it can and leaves the rest NULL). So three further guards keep this cron
 * off any account that shows another sign of once having been real:
 *
 *  - a stamped lastLogin — you cannot sign in to an account that was never
 *    activated, so this is proof of activation the backfill could not have;
 *  - an explicit deletion hold (dontDeleteBefore), as the data-erase path honours;
 *  - age — an abandoned registration is abandoned within days (the confirmation
 *    link is valid for three), so a row that has sat disabled and unactivated far
 *    longer is a historical account whose NULL predates reliable tracking, not a
 *    fresh drop-off. Only registrations from the recent window are reaped.
 */
#[AsCronJob('daily')]
class TruncateUnactivatedUsers
{
    public function __construct(private readonly UserRepository $users)
    {
    }

    public function __invoke(string $scope): void
    {
        if (Cron::SCOPE_WEB === $scope) {
            return;
        }

        $this->users->createQueryBuilder('user')
            ->delete()
            ->where('user.disable = 1')
            ->andWhere('user.activatedAt IS NULL')
            ->andWhere('user.lastLogin IS NULL')
            ->andWhere('user.dontDeleteBefore IS NULL OR user.dontDeleteBefore < CURRENT_TIMESTAMP()')
            ->andWhere('user.createdAt <= :graceCutoff')
            ->andWhere('user.createdAt >= :abandonWindowStart')
            ->setParameter('graceCutoff', new \DateTimeImmutable('-4 days'))
            ->setParameter('abandonWindowStart', new \DateTimeImmutable('-30 days'))
            ->getQuery()
            ->execute()
        ;
    }
}
