<?php

namespace App\Service\Cron\Job;

use App\Entity\Core\File;
use App\Enum\Core\FileHostEnum;
use App\Enum\Core\FileStatusEnum;
use App\Repository\Core\FileRepository;
use App\Service\Cron\BaseCronJob;
use App\Service\Cron\CronjobInterface;
use App\Service\File\Storage\LocalStorage;
use App\Service\Utils\DatesUtils;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\ConnectionException;
use Doctrine\ORM\QueryBuilder;
use Exception;
use JetBrains\PhpStorm\Pure;

/**
 * Cronjob that delete temporary files older than N days
 *
 * >>> ot:cron clean-temp-files
 */
class CleanTempFiles extends BaseCronJob implements CronjobInterface
{
    /**
     * Delay before removing a temporary file (in days)
     */
    private const DELETE_OLDER_THAN = 60;

    /**
     * @param Connection $connection
     * @param FileRepository $fileRepository
     * @param LocalStorage $storage
     */
    #[Pure]
    public function __construct(
        private Connection $connection,
        private FileRepository $fileRepository,
        private LocalStorage $storage
    ) {
        parent::__construct();
    }

    /**
     * Preview the result of the execution, without actually deleting anything
     * @throws Exception
     */
    public function preview(): void
    {
        $maxDate = DatesUtils::new();
        $maxDate->sub(new \DateInterval('P' . self::DELETE_OLDER_THAN . 'D'));

        $files = $this->listFilesToDelete($maxDate);
        $total = count($files);
        $this->ui->print($total . " temporary files to be removed");
        $this->ui->print("> Printing the first and last 50 :");

        $i = 0;
        foreach ($files as $file) {
            $i++;
            if ($i > 50 && ($total - $i) > 50) { continue; }
            if (($total - $i) === 50) {
                $this->ui->print('  (...)');
            }
            $this->ui->print('  * ' . $file->getPath());
        }

        $this->purgeDb($maxDate, false);
    }

    /**
     * Proceed to the deletion of the files and the purge of the DB
     * @throws Exception
     */
    public function execute(): void
    {
        $maxDate = DatesUtils::new();
        $maxDate->sub(new \DateInterval('P' . self::DELETE_OLDER_THAN . 'D'));

        $files = $this->listFilesToDelete($maxDate);
        $this->deleteFiles($files);
        $this->purgeDb($maxDate);
    }

    /**
     * List the files to delete in the DB
     * @param \DateTime $maxDate
     * @return Collection<File>
     * @throws Exception
     */
    protected function listFilesToDelete(\DateTime $maxDate): Collection
    {
        $this->ui->print('List temporary files created before ' . $maxDate->format('c'));

        $queryBuilder = $this->fileRepository->createQueryBuilder('f');
        $queryBuilder->select();
        $this->getQueryConditions($queryBuilder, $maxDate);
        return $queryBuilder->getQuery()->getResult();
    }

    /**
     * Purge the DB from temporary file records older than N days
     *
     * @param \DateTime $maxDate
     * @param bool $commit
     * @throws ConnectionException
     */
    protected function purgeDb(\DateTime $maxDate, bool $commit = true, ): void {
        $this->connection->beginTransaction();
        $this->connection->setAutoCommit(false);

        $purged = 0;

        try{
            $purged += $this->purgeFiles($maxDate);

            if ($commit) {
                $this->connection->commit();
                $this->ui->print('DB purged - ' . $purged . ' records permanently deleted');
            }else{
                $this->connection->rollback();
                $this->ui->print('DB purged - ' . $purged . ' records would be permanently deleted');
            }
        }catch (\Exception $exception){
            $this->connection->rollback();
            throw $exception;
        }
    }

    /**
     * Purge File tables and returns the number of deleted records
     *
     * @param $maxDate
     * @return int
     */
    protected function purgeFiles(\DateTime $maxDate): int{
        $queryBuilder = $this->fileRepository->createQueryBuilder('f');
        $queryBuilder->delete();
        $this->getQueryConditions($queryBuilder, $maxDate);
        return $queryBuilder->getQuery()->execute();
    }

    /**
     * Delete the files
     *
     * @param Collection<File> $files
     * @return void
     */
    protected function deleteFiles(Collection $files): void {
        $total = count($files);
        $this->ui->print($total . " temporary files to be removed");

        $this->ui->print('Deleting files...');
        $i = 0;
        $deleted = 0;
        $this->ui->progress(0, $total);
        foreach ($files as $file) {
            try {
                $i++;
                $this->ui->progress($i, $total);
                $this->storage->hardDelete($file);
                $deleted++;
            } catch (\RuntimeException $e) {
                $this->ui->print('ERROR : ' . $e->getMessage());
            }
        }

        $this->ui->print($deleted . ' files deleted');
    }

    /**
     * @param QueryBuilder $queryBuilder
     * @param \DateTime $maxDate
     */
    protected function getQueryConditions(QueryBuilder $queryBuilder, \DateTime $maxDate): void{
        $queryBuilder
            ->andWhere(
                $queryBuilder->expr()->orX(
                    $queryBuilder->expr()->eq('f.isTemporaryFile', ':temporaryTrue'),
                    $queryBuilder->expr()->eq('f.status', ':status')
                )
            )
            ->andWhere($queryBuilder->expr()->eq('f.host', ':host'))
            ->andWhere(
                $queryBuilder->expr()->orX(
                    $queryBuilder->expr()->lt('f.createDate', ':maxDate'),
                    $queryBuilder->expr()->isNull('f.createDate')
                )
            )
            ->setParameter('temporaryTrue', true)
            ->setParameter('host', FileHostEnum::AP2I()->getValue())
            ->setParameter('status', FileStatusEnum::DELETED()->getValue())
            ->setParameter('maxDate', $maxDate->format('Y-m-d'))
        ;
    }
}
