<?php

namespace App\Console\Commands;

use App\Models\PlanFamily;
use App\Models\PlanPrice;
use App\Models\PlanVersion;
use App\Services\Stripe\StripeClient;
use Illuminate\Console\Command;

/**
 * Mirrors our catalogue into Stripe: a Product per plan family, a Price per
 * priced row.
 *
 * Stripe owns the recurring billing — retries, dunning, off-session SCA,
 * invoice numbering — so it needs to know what it is billing for. It does not
 * need to know how big the VM is, and it is not asked.
 *
 * Idempotent by construction: a row that already carries an id is skipped. That
 * matters more here than usual, because a Stripe Price cannot be edited, so a
 * second run that minted duplicates would leave two live prices for one plan
 * and no way to tell which a customer is on.
 *
 * Only PUBLISHED versions are synced. A draft has promised nothing, and a
 * Product for it would be a price list entry for something that may never
 * exist.
 */
class SyncStripeCatalogue extends Command
{
    protected $signature = 'stripe:sync-catalogue {--dry-run : Show what would be created without touching Stripe}';

    protected $description = 'Create the Stripe products and prices for the plan catalogue';

    public function handle(StripeClient $stripe): int
    {
        $dryRun = (bool) $this->option('dry-run');

        if (! $dryRun && ! $stripe->isConfigured()) {
            $this->error('Stripe is not configured (STRIPE_SECRET is empty). Nothing was created.');

            return self::FAILURE;
        }

        $created = 0;

        foreach (PlanFamily::query()->with('versions.prices')->orderBy('tier')->get() as $family) {
            $published = $family->versions->filter(fn (PlanVersion $version) => $version->isPublished());

            // A family whose versions are all drafts has promised nothing, so
            // it has no business appearing in Stripe's price list yet.
            if ($published->isEmpty()) {
                continue;
            }

            $productId = $family->stripe_product_id;

            if ($productId === null) {
                $this->line("  product  {$family->key} — {$family->name}");
                $created++;

                if (! $dryRun) {
                    $productId = $stripe->createProduct(
                        $family->name,
                        ['plan_family' => $family->key, 'plan_family_id' => (string) $family->id],
                        // Keyed on our row, so a crash between Stripe creating
                        // the product and us storing its id gives back the same
                        // product on the next run rather than a second one.
                        idempotencyKey: "clupilot-product-{$family->id}",
                    );
                    $family->update(['stripe_product_id' => $productId]);
                }
            }

            foreach ($published as $version) {
                foreach ($version->prices as $price) {
                    if ($price->stripe_price_id !== null) {
                        continue;
                    }

                    $this->line(sprintf(
                        '  price    %s v%d %s %d %s',
                        $family->key, $version->version, $price->term, $price->amount_cents, $price->currency,
                    ));
                    $created++;

                    if ($dryRun || $productId === null) {
                        continue;
                    }

                    $priceId = $stripe->createPrice(
                        productId: $productId,
                        amountCents: $price->amount_cents,
                        currency: $price->currency,
                        interval: $price->term === 'yearly' ? 'year' : 'month',
                        metadata: [
                            'plan_family' => $family->key,
                            'plan_version' => (string) $version->version,
                            'plan_version_id' => (string) $version->id,
                            'plan_price_id' => (string) $price->id,
                        ],
                        idempotencyKey: "clupilot-price-{$price->id}",
                    );

                    // Written straight through the query builder: stripe_price_id
                    // is not part of what publication froze, and the model would
                    // otherwise have to be re-read first.
                    PlanPrice::query()->whereKey($price->id)->update(['stripe_price_id' => $priceId]);
                }
            }
        }

        $this->newLine();

        if ($created === 0) {
            $this->info('Stripe is already in step with the catalogue.');

            return self::SUCCESS;
        }

        $this->info($dryRun
            ? "{$created} object(s) would be created. Run without --dry-run to create them."
            : "{$created} object(s) created in Stripe.");

        return self::SUCCESS;
    }

    /** Versions whose prices are live in Stripe, for the status line. */
    public static function syncedVersions(): int
    {
        return PlanVersion::query()
            ->whereNotNull('published_at')
            ->whereHas('prices', fn ($q) => $q->whereNotNull('stripe_price_id'))
            ->count();
    }
}
