<?php

namespace App\Console\Commands;

use App\Models\Subscription;
use App\Services\AuditLogger;
use App\Services\Billing\BillingManager;
use Illuminate\Console\Command;
use Throwable;

/**
 * Generic guardrail for subscriptions whose lifecycle webhook never reached
 * us. Targets any provider's rows in `incomplete` / `past_due` state with
 * `current_period_ends_at` past the threshold AND not already canceled.
 *
 * Different from `razorpay:cleanup-stale-pending-subs` (the Razorpay-specific
 * `pending` sweep) — that one cleans up state from the cancel-then-create
 * fallback in Razorpay plan changes. This one catches the broader "provider
 * sent .deleted but webhook delivery failed" gap across every provider.
 *
 * Stripe normally auto-expires `incomplete` subs after 23h and fires
 * `customer.subscription.deleted`. If a local row sits in that state for
 * 30+ days, the webhook was dropped or never reached us — sync the local
 * state to match what the provider has long since done.
 *
 * Cancel attempt on the provider side is best-effort: if it rejects (already
 * canceled, expired, unknown id), the local row is still marked canceled so
 * the same row isn't retried every week forever. The provider-side state is
 * already past its useful life.
 */
class CleanupStaleSubscriptionsCommand extends Command
{
    protected $signature = 'subscriptions:cleanup-stale
                            {--days=30 : current_period_ends_at threshold in days}
                            {--max-time=55 : Max execution time in seconds}
                            {--dry-run : Report without canceling}';

    protected $description = 'Cancel local subs in incomplete/past_due state whose period has long since ended (lifecycle webhook was missed).';

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

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

        $query = Subscription::query()
            ->whereIn('status', ['incomplete', 'past_due'])
            ->where('current_period_ends_at', '<', $cutoff)
            ->whereNull('canceled_at')
            ->orderBy('id');

        $eligible = (int) $query->count();
        $this->line("Cutoff: {$cutoff->toIso8601String()} ({$days} days)");
        $this->line("Eligible rows: {$eligible}");

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

        if ($dryRun) {
            foreach ($query->limit(50)->get() as $sub) {
                $this->line(sprintf(
                    '[dry-run] would cancel sub #%d (%s:%s) status=%s period_ended=%s',
                    $sub->id,
                    $sub->provider_key,
                    $sub->provider_subscription_id ?? '(no provider id)',
                    $sub->status,
                    $sub->current_period_ends_at?->toDateString() ?? '?',
                ));
            }
            if ($eligible > 50) {
                $this->line(sprintf('... and %d more', $eligible - 50));
            }

            return self::SUCCESS;
        }

        $canceled = 0;
        $providerErrors = 0;
        $skippedLive = 0;
        $stoppedEarly = false;

        foreach ($query->cursor() as $sub) {
            if (microtime(true) >= $deadline) {
                $stoppedEarly = true;
                break;
            }

            try {
                $provider = $manager->provider($sub->provider_key);
            } catch (Throwable $e) {
                // Unknown provider key → can't verify, so DON'T cancel.
                $providerErrors++;
                $this->warn(sprintf('  unknown provider for sub #%d (%s): %s', $sub->id, $sub->provider_key, $e->getMessage()));
                continue;
            }

            // Authoritative re-check before any irreversible cancel: a local
            // `past_due` the provider's dunning has since RECOVERED to active
            // (missed recovery webhook) must not be killed. A row with no
            // provider id can't be live, so it's safe to sync. Otherwise:
            //   true  = live/recoverable → skip,
            //   null  = couldn't determine → skip (conservative),
            //   false = provider-terminal → safe to cancel + sync.
            if (! $sub->provider_subscription_id) {
                $isLive = false;
            } else {
                try {
                    $isLive = $provider->fetchSubscriptionIsLive($sub);
                } catch (Throwable $e) {
                    $isLive = null;
                    $this->warn(sprintf('  status fetch failed for sub #%d (%s): %s', $sub->id, $sub->provider_key, $e->getMessage()));
                }
            }

            if ($isLive !== false) {
                $skippedLive++;
                $this->line(sprintf(
                    '  skipped sub #%d (%s): provider reports %s — not canceling',
                    $sub->id,
                    $sub->provider_key,
                    $isLive === true ? 'live' : 'indeterminate',
                ));
                continue;
            }

            // Provider confirms terminal — best-effort cancel (idempotent if
            // already gone), then sync the local row.
            try {
                $provider->cancelSubscription($sub, atPeriodEnd: false);
            } catch (Throwable $e) {
                $providerErrors++;
                $this->warn(sprintf(
                    '  provider cancel failed for sub #%d (%s): %s',
                    $sub->id,
                    $sub->provider_key,
                    $e->getMessage(),
                ));
            }

            $sub->forceFill([
                'status' => 'canceled',
                'canceled_at' => $sub->canceled_at ?? now(),
            ])->save();

            $canceled++;
        }

        AuditLogger::record(
            'subscription.cleanup_stale',
            null,
            "Marked {$canceled} stale sub(s) canceled (>{$days} days past period end; skipped {$skippedLive} still-live/indeterminate; {$providerErrors} provider errors)",
            [
                'days' => $days,
                'canceled' => $canceled,
                'skipped_live' => $skippedLive,
                'provider_errors' => $providerErrors,
                'stopped_early' => $stoppedEarly,
            ],
        );

        $this->info("Canceled {$canceled} sub(s); skipped {$skippedLive} live/indeterminate; provider errors: {$providerErrors}.");
        if ($stoppedEarly) {
            $this->warn('Stopped at deadline — re-run to continue.');
        }

        return self::SUCCESS;
    }
}
