<?php

namespace App\Services\Billing;

use App\Contracts\BillingProvider;
use App\Models\PaymentProvider;
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Collection;

/**
 * Registry + resolver for every shipped `BillingProvider` implementation.
 *
 * Two layers govern whether a provider can serve real traffic:
 *   1. Code registration (`register()`) — the class exists in the codebase.
 *   2. DB activation (`payment_providers.is_active`) — the deployment admin
 *      has pasted credentials and flipped the row on.
 *
 * `activeProviders()` intersects both — a provider that's registered but not
 * activated stays dormant; an activated row whose code was removed is silently
 * skipped (graceful uninstall path).
 *
 * Provider classes are constructed via the container so each call returns a
 * fresh instance hydrated with its own `PaymentProvider $row` — call sites
 * MUST NOT cache provider instances across DB rows.
 */
class BillingManager
{
    /** @var array<string, class-string<BillingProvider>> */
    private array $registry = [];

    public function __construct(private Container $container) {}

    /**
     * Register a provider class. Called once at boot via the BillingServiceProvider
     * for each shipped provider. Adding a new provider = a new registration line.
     */
    public function register(string $key, string $providerClass): void
    {
        if (! is_subclass_of($providerClass, BillingProvider::class)) {
            throw new \InvalidArgumentException("Class [{$providerClass}] must implement BillingProvider.");
        }
        $this->registry[$key] = $providerClass;
    }

    public function provider(string $key): BillingProvider
    {
        if (! isset($this->registry[$key])) {
            throw new \InvalidArgumentException("Unknown billing provider [{$key}].");
        }

        /** @var BillingProvider $instance */
        $instance = $this->container->make($this->registry[$key]);

        $row = PaymentProvider::query()->where('key', $key)->first();
        if ($row !== null && method_exists($instance, 'withRow')) {
            $instance->withRow($row);
        }

        return $instance;
    }

    /** @return Collection<int, BillingProvider>  Active rows in the DB AND registered in code. */
    public function activeProviders(): Collection
    {
        return PaymentProvider::query()
            ->where('is_active', true)
            ->orderBy('position')
            ->get()
            ->filter(fn (PaymentProvider $row) => isset($this->registry[$row->key]))
            ->map(fn (PaymentProvider $row) => $this->provider($row->key))
            ->values();
    }

    /** @return array<int, string> All registered keys, regardless of activation. */
    public function registeredKeys(): array
    {
        return array_keys($this->registry);
    }

    /**
     * Human-readable labels for every registered provider, in registration
     * order. Used by UI copy that lists supported providers ("Stripe and
     * Razorpay credentials") — derives from the registry so adding a new
     * provider doesn't require touching every user-facing string.
     *
     * @return array<int, string>
     */
    public function registeredLabels(): array
    {
        return array_map(
            fn (string $key) => $this->provider($key)->label(),
            $this->registeredKeys(),
        );
    }

    /**
     * Join the registered provider labels into a natural-language list:
     *   ['Stripe']                       → "Stripe"
     *   ['Stripe', 'Razorpay']           → "Stripe and Razorpay"
     *   ['Stripe', 'Razorpay', 'Adyen']  → "Stripe, Razorpay, and Adyen"
     *
     * Use `$conjunction='or'` when the copy needs disjunctive sense
     * ("Add Stripe or Razorpay credentials").
     */
    public function humanProviderList(string $conjunction = 'and'): string
    {
        $labels = $this->registeredLabels();
        $n = count($labels);
        if ($n === 0) {
            return '';
        }
        if ($n === 1) {
            return $labels[0];
        }
        if ($n === 2) {
            return "{$labels[0]} {$conjunction} {$labels[1]}";
        }

        $last = array_pop($labels);

        return implode(', ', $labels).", {$conjunction} {$last}";
    }

    /**
     * Find which active provider owns this incoming webhook by signature
     * detection. Returns null if no active provider matches — the controller
     * abort(400)s in that case.
     */
    public function matchWebhook(\Illuminate\Http\Request $request): ?BillingProvider
    {
        return $this->activeProviders()->first(
            fn (BillingProvider $p) => $p->matchesWebhook($request),
        );
    }
}
