<?php

namespace App\Console\Commands;

use App\Models\Campaign;
use App\Services\Queue\QueueHealthService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;

/**
 * Defence in depth for the "stuck in sending" bug.
 *
 * The pre-flight check in CampaignService::sendCampaign() blocks NEW
 * sends when the queue worker is dead. This command handles the OLD
 * sends that flipped to 'sending' before the pre-flight existed (or
 * before the worker died mid-send) so they don't sit in the UI as
 * "sending" forever.
 *
 * A campaign is considered stuck when ALL of these are true:
 *   1. status = 'sending'
 *   2. sent_at is older than the threshold (default 15 min)
 *   3. ZERO recipients have moved past `pending` — i.e. no job ever ran
 *
 * Stuck campaigns are reverted to 'draft' with a clear reason in
 * admin_notes so the user can re-launch once the queue is healthy.
 *
 * Campaigns where some recipients have already shipped are left alone
 * — they're legitimately mid-flight, not stuck.
 *
 * Scheduled in routes/console.php at every 5 minutes.
 */
class ReapStuckCampaignsCommand extends Command
{
    protected $signature = 'campaigns:reap-stuck
                            {--threshold-minutes=15 : Treat sends older than this many minutes as candidates.}
                            {--dry-run : Print what would change without writing.}';

    protected $description = 'Revert campaigns stuck in "sending" with no progress (queue worker was down).';

    public function handle(QueueHealthService $health): int
    {
        $thresholdMinutes = (int) $this->option('threshold-minutes');
        $dryRun = (bool) $this->option('dry-run');

        $cutoff = now()->subMinutes(max(1, $thresholdMinutes));

        // Candidate campaigns: in "sending" since at least the threshold,
        // with no successful or in-flight recipients. The
        // whereDoesntHave excludes any campaign whose pipeline produced
        // even one row past the `pending` state — those are legitimately
        // mid-send and should NOT be reverted.
        $stuck = Campaign::query()
            ->where('status', 'sending')
            ->where('sent_at', '<', $cutoff)
            ->whereDoesntHave('campaignRecipients', function ($q) {
                $q->whereNotIn('status', ['pending']);
            })
            ->get();

        if ($stuck->isEmpty()) {
            $this->info('No stuck campaigns found.');
            return self::SUCCESS;
        }

        $queueStatus = $health->status('campaigns');
        $this->line(sprintf(
            'Queue status: %s | driver=%s | pending=%d | stale=%d | heartbeat_age=%s',
            $queueStatus['alive'] ? 'ALIVE' : 'DOWN',
            $queueStatus['driver'],
            $queueStatus['pending_jobs'],
            $queueStatus['stale_jobs'],
            $queueStatus['heartbeat_age_s'] ?? 'never'
        ));
        $this->newLine();

        $reason = sprintf(
            'Auto-reverted by campaigns:reap-stuck — no recipient processed within %d minutes. '
            . 'The background queue worker was likely not running. '
            . 'Start it with: php artisan queue:work --queue=campaigns,default',
            $thresholdMinutes
        );

        $reverted = 0;
        foreach ($stuck as $campaign) {
            $this->line(sprintf(
                '%s campaign #%d "%s" (workspace=%d, sent_at=%s, recipients=%d)',
                $dryRun ? '[DRY-RUN]' : 'REVERT',
                $campaign->id,
                $campaign->name,
                $campaign->workspace_id,
                $campaign->sent_at?->toIso8601String() ?? '?',
                (int) $campaign->recipients_count
            ));

            if ($dryRun) {
                continue;
            }

            // Wipe the stale recipient rows from the aborted attempt so
            // a relaunch starts clean. If we leave them, the re-send
            // path's `delete()` inside the transaction would handle it
            // — but doing it here makes the campaign list show an
            // accurate recipient count immediately.
            $campaign->campaignRecipients()->delete();

            $campaign->update([
                'status'           => 'draft',
                'sent_at'          => null,
                'recipients_count' => 0,
                'admin_notes'      => trim(($campaign->admin_notes ?? '') . "\n\n" . $reason),
            ]);

            Log::warning('Reverted stuck campaign to draft', [
                'campaign_id'   => $campaign->id,
                'workspace_id'  => $campaign->workspace_id,
                'threshold_min' => $thresholdMinutes,
                'queue_status'  => $queueStatus,
            ]);

            $reverted++;
        }

        $this->newLine();
        $this->info(sprintf(
            '%s %d / %d campaigns.',
            $dryRun ? 'Would revert' : 'Reverted',
            $dryRun ? $stuck->count() : $reverted,
            $stuck->count()
        ));

        return self::SUCCESS;
    }
}
