<?php

namespace App\Console\Commands;

use App\Models\WebhookEvent;
use App\Services\AuditLogger;
use Illuminate\Console\Command;

/**
 * Retention sweep for the `webhook_events` table. Every row represents a
 * successfully-dispatched provider webhook (Stripe / Razorpay); failed
 * dispatches don't persist (the transaction rolls back), so there's no
 * "failed status" to preserve separately — all rows are eligible to age
 * out at the same horizon.
 *
 * Default 90 days matches what Stripe / Razorpay themselves keep visible
 * in their dashboards. Anything older has long-since stopped mattering
 * for "did this event get processed?" diagnosis; the authoritative record
 * is the provider dashboard anyway.
 *
 * The earlier `webhookevent.replay.failed` audit-log entries are NOT
 * touched — those live on `audit_logs` and follow that table's own
 * retention (`auditlog:purge --days=730`).
 *
 * Scheduled weekly. Self-hosted installs benefit too — busy
 * installs accumulate hundreds of events per day and the JSON payloads
 * (1-10 KB each) add up.
 */
class WebhookEventsPurgeCommand extends Command
{
    protected $signature = 'webhookevents:purge
                            {--dry-run : Print what would be deleted without writing}
                            {--days=90 : Delete rows whose processed_at is older than N days}
                            {--max-time=55 : Stop after N seconds — keeps the run under shared-hosting exec limits}';

    protected $description = 'Delete webhook_events rows older than the retention window.';

    public function handle(): int
    {
        $dryRun = (bool) $this->option('dry-run');
        $days = max(1, (int) $this->option('days'));
        $maxTime = max(5, (int) $this->option('max-time'));

        @set_time_limit($maxTime + 5);
        $deadline = microtime(true) + $maxTime;
        $cutoff = now()->subDays($days);

        $eligible = WebhookEvent::query()
            ->where('processed_at', '<', $cutoff)
            ->count();

        $this->line(sprintf(
            'Cutoff: %s (%d days). Eligible rows: %d',
            $cutoff->toIso8601String(),
            $days,
            $eligible,
        ));

        if ($eligible === 0) {
            return self::SUCCESS;
        }

        if ($dryRun) {
            $this->line('[dry-run] no rows deleted.');

            return self::SUCCESS;
        }

        // Chunked delete so the run can break out on the deadline. ID is
        // a reasonable proxy for "old" because rows are inserted strictly
        // in processed_at order — but we filter on processed_at directly
        // to stay correct under any future out-of-order insert.
        $deleted = 0;
        $stoppedEarly = false;

        while (microtime(true) < $deadline) {
            $ids = WebhookEvent::query()
                ->where('processed_at', '<', $cutoff)
                ->orderBy('id')
                ->limit(500)
                ->pluck('id')
                ->all();

            if ($ids === []) {
                break;
            }

            $deleted += WebhookEvent::query()->whereIn('id', $ids)->delete();

            if (microtime(true) >= $deadline) {
                $stoppedEarly = true;
                break;
            }
        }

        AuditLogger::record(
            'webhookevent.purge',
            null,
            "Purged {$deleted} webhook event(s) older than {$days} days",
            [
                'days' => $days,
                'cutoff' => $cutoff->toIso8601String(),
                'deleted' => $deleted,
                'stopped_early' => $stoppedEarly,
            ],
        );

        $this->line(sprintf('Deleted: %d', $deleted));
        if ($stoppedEarly) {
            $this->warn('Stopped at deadline — re-run to continue.');
        }

        return self::SUCCESS;
    }
}
