<?php

namespace App\Services\Stripe;

use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
use RuntimeException;

/**
 * Stripe's REST API over the HTTP client, rather than the SDK: this touches
 * three endpoints, and a fake for the tests is easier to trust than a mocked
 * library. Not unit-tested against the real service (live I/O).
 */
class HttpStripeClient implements StripeClient
{
    public function isConfigured(): bool
    {
        return filled($this->secret());
    }

    public function createProduct(string $name, array $metadata = [], ?string $idempotencyKey = null): string
    {
        return (string) $this->request($idempotencyKey)
            ->asForm()
            ->post($this->url('products'), array_filter([
                'name' => $name,
                ...$this->flatten('metadata', $metadata),
            ]))
            ->throw()
            ->json('id');
    }

    public function createPrice(
        string $productId,
        int $amountCents,
        string $currency,
        string $interval,
        array $metadata = [],
        ?string $idempotencyKey = null,
    ): string {
        return (string) $this->request($idempotencyKey)
            ->asForm()
            ->post($this->url('prices'), [
                'product' => $productId,
                'unit_amount' => $amountCents,
                'currency' => strtolower($currency),
                // Recurring, because a plan is a subscription. The interval
                // belongs to the Price, which is why monthly and yearly cannot
                // share one.
                'recurring[interval]' => $interval,
                ...$this->flatten('metadata', $metadata),
            ])
            ->throw()
            ->json('id');
    }

    public function archivePrice(string $priceId): void
    {
        // Deactivated, never deleted: a Stripe Price is immutable and existing
        // subscriptions keep billing against it. That is the same
        // grandfathering our own catalogue does, enforced on their side too.
        $this->request()
            ->asForm()
            ->post($this->url('prices/'.$priceId), ['active' => 'false'])
            ->throw();
    }

    private function request(?string $idempotencyKey = null): PendingRequest
    {
        $secret = (string) $this->secret();

        if (blank($secret)) {
            throw new RuntimeException('Stripe is not configured (STRIPE_SECRET is empty).');
        }

        $request = Http::withToken($secret)->acceptJson()->timeout(20);

        // Stripe replays the original response for a repeated key instead of
        // creating a second object. That covers the gap this cannot close on
        // its own: a crash between Stripe creating a Price and us storing its
        // id. Their keys expire after 24 hours, so it protects a retry, not a
        // sync re-run next week — for which the stored ids are the guard.
        return $idempotencyKey !== null
            ? $request->withHeaders(['Idempotency-Key' => $idempotencyKey])
            : $request;
    }

    /**
     * The key in force: the one stored in the console if there is one, else the
     * environment.
     *
     * Resolved HERE, at the point of use, rather than overlaid onto config at
     * boot — an overlay would add a query to every request including the public
     * site, and a queue worker would keep whatever was true when it started.
     */
    private function secret(): ?string
    {
        return app(\App\Services\Secrets\SecretVault::class)->get('stripe.secret');
    }

    private function url(string $path): string
    {
        return rtrim((string) config('services.stripe.api_base'), '/').'/'.$path;
    }

    /**
     * Stripe's form API takes nested keys as `metadata[key]`.
     *
     * @param  array<string, string|int|null>  $values
     * @return array<string, string>
     */
    private function flatten(string $prefix, array $values): array
    {
        $flat = [];

        foreach ($values as $key => $value) {
            if ($value !== null && $value !== '') {
                $flat["{$prefix}[{$key}]"] = (string) $value;
            }
        }

        return $flat;
    }
}
