<?php

declare(strict_types=1);

namespace App\Shared\Infrastructure\Console;

use App\Shared\Domain\Clock;
use DateTimeImmutable;
use Doctrine\DBAL\Connection;
use Override;

use function sprintf;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

/**
 * Archives audit log entries older than 5 years.
 *
 * Entries are MOVED (not copied) to audit_log_archive table.
 * This preserves immutability while managing table size.
 *
 * WARNING: This command temporarily disables PostgreSQL rules via ALTER TABLE,
 * which acquires an AccessExclusiveLock. Schedule during LOW TRAFFIC periods only.
 *
 * Run via CRON: 0 2 * * 0 php bin/console app:audit:archive
 *
 * @see Story 1.7 - T8: Archivage
 * @see NFR-C5: Conservation 5 ans actif, puis archive
 */
#[AsCommand(
    name: 'app:audit:archive',
    description: 'Archive audit log entries older than 5 years',
)]
final class ArchiveAuditLogsCommand extends Command
{
    private const int DEFAULT_RETENTION_YEARS = 5;
    private const int DEFAULT_BATCH_SIZE = 1000;

    public function __construct(
        private readonly Connection $connection,
        private readonly Clock $clock,
    ) {
        parent::__construct();
    }

    #[Override]
    protected function configure(): void
    {
        $this
            ->addOption(
                'retention-years',
                'r',
                InputOption::VALUE_REQUIRED,
                'Number of years to keep in active table',
                (string) self::DEFAULT_RETENTION_YEARS,
            )
            ->addOption(
                'batch-size',
                'b',
                InputOption::VALUE_REQUIRED,
                'Number of records to process per batch',
                (string) self::DEFAULT_BATCH_SIZE,
            )
            ->addOption(
                'dry-run',
                null,
                InputOption::VALUE_NONE,
                'Simulate without making changes',
            );
    }

    #[Override]
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);

        /** @var string $retentionYearsOption */
        $retentionYearsOption = $input->getOption('retention-years');
        $retentionYears = (int) $retentionYearsOption;

        if ($retentionYears < 1) {
            $io->error('Retention years must be a positive integer.');

            return Command::FAILURE;
        }

        /** @var string $batchSizeOption */
        $batchSizeOption = $input->getOption('batch-size');
        $batchSize = (int) $batchSizeOption;

        if ($batchSize < 1) {
            $io->error('Batch size must be a positive integer.');

            return Command::FAILURE;
        }

        $dryRun = (bool) $input->getOption('dry-run');

        $cutoffDate = $this->clock->now()->modify("-{$retentionYears} years");
        $io->title('Audit Log Archival');
        $io->info(sprintf(
            'Archiving entries older than %s (%d years retention)',
            $cutoffDate->format('Y-m-d H:i:s'),
            $retentionYears,
        ));

        if ($dryRun) {
            $io->warning('DRY RUN MODE - No changes will be made');
        }

        // Count entries to archive
        $countSql = 'SELECT COUNT(*) FROM audit_log WHERE occurred_at < :cutoff';
        /** @var string|int|false $countResult */
        $countResult = $this->connection->fetchOne($countSql, [
            'cutoff' => $cutoffDate->format('Y-m-d H:i:s.uP'),
        ]);
        $totalCount = (int) $countResult;

        if ($totalCount === 0) {
            $io->success('No entries to archive.');

            return Command::SUCCESS;
        }

        $io->info(sprintf('Found %d entries to archive', $totalCount));

        if ($dryRun) {
            $io->success(sprintf('DRY RUN: Would archive %d entries', $totalCount));

            return Command::SUCCESS;
        }

        // Archive in batches
        $progressBar = $io->createProgressBar($totalCount);
        $progressBar->start();
        $archivedCount = 0;

        do {
            $batchCount = $this->archiveBatch($cutoffDate, $batchSize);
            $archivedCount += $batchCount;
            $progressBar->advance($batchCount);
        } while ($batchCount === $batchSize);

        $progressBar->finish();
        $io->newLine(2);

        $io->success(sprintf(
            'Successfully archived %d audit log entries',
            $archivedCount,
        ));

        return Command::SUCCESS;
    }

    /**
     * Archive a batch of entries using PostgreSQL function.
     *
     * The archive_audit_entries() function is SECURITY DEFINER and can
     * bypass the audit_no_delete rule. This is the ONLY way to delete
     * from audit_log - ensuring immutability for normal operations.
     */
    private function archiveBatch(DateTimeImmutable $cutoffDate, int $batchSize): int
    {
        // Call the PostgreSQL function that handles INSERT + DELETE atomically
        // The function uses SECURITY DEFINER to bypass the delete rule
        $sql = 'SELECT archive_audit_entries(:cutoff, :batch_size)';

        /** @var int|string|false $result */
        $result = $this->connection->fetchOne($sql, [
            'cutoff' => $cutoffDate->format('Y-m-d H:i:s.uP'),
            'batch_size' => $batchSize,
        ]);

        return (int) $result;
    }
}
