<?php

namespace App\Contracts;

use App\Models\Invoice;
use App\Models\Plan;
use App\Models\Subscription;
use App\Models\User;
use Illuminate\Http\Request;

/**
 * Pluggable billing provider. Drop a class implementing this interface and
 * register it in the BillingManager (auto-discovered from the providers/
 * directory) — admin can then activate it in /admin/billing once API keys are
 * pasted into the encrypted `payment_providers.config` JSON.
 *
 * The single `POST /webhooks/billing` endpoint iterates active providers and
 * dispatches the request to whichever one's `matchesWebhook()` returns true,
 * so the signature-detection rule must be unambiguous (header or body shape
 * unique to that provider — never an IP-based check).
 */
interface BillingProvider
{
    /** Stable identifier — matches `payment_providers.key` and `subscriptions.provider_key`. */
    public function key(): string;

    /** Human-readable label for the admin UI + checkout buttons. */
    public function label(): string;

    /**
     * Whether this provider is currently configured in test/sandbox mode.
     * Drives the `is_test` tag persisted on every subscription + invoice
     * row at create time, which in turn powers the admin "Clear test data"
     * tool. Default: false (treat unknown as live so we never accidentally
     * sweep production rows).
     */
    public function isTestMode(): bool;

    /**
     * Whether this incoming HTTP request looks like a webhook from THIS provider.
     * Inspect signature headers (e.g. `Stripe-Signature`, `X-Razorpay-Signature`)
     * — NEVER body content alone, since multiple providers can ship overlapping
     * payload shapes. Returning true commits the controller to using this
     * provider for verification; if verification fails the request gets a 400.
     */
    public function matchesWebhook(Request $request): bool;

    /**
     * Verify the webhook payload's signature against the secret stored in
     * `payment_providers.config`. Throws `\App\Exceptions\InvalidWebhookSignature`
     * on mismatch. Returns the decoded event payload.
     *
     * @return array<string, mixed>
     */
    public function verifyWebhook(string $payload, Request $request): array;

    /**
     * Translate a verified provider event into domain events
     * (`SubscriptionActivated`, `SubscriptionCanceled`, `InvoicePaid`, etc.)
     * and persist any state changes to `subscriptions` / `invoices`. Idempotent:
     * replayed events MUST NOT double-charge or duplicate rows.
     *
     * @param  array<string, mixed>  $event
     */
    public function dispatchEvent(array $event): void;

    /**
     * Create a hosted checkout session for `$user` to subscribe to `$plan` at
     * `$period` (monthly|yearly). Returns the URL the user redirects to.
     * The session lookup-id is also persisted on the user's pending
     * subscription row so the webhook can finish the wiring.
     *
     * `$currencyId` selects the row in `plan_prices` to use — when null,
     * implementations fall back to the deployment's default currency.
     * Callers wiring a fresh checkout from /pricing.json pass the
     * user-selected currency; admin-initiated change-plan flows resolve
     * from the existing subscription's `currency_id`.
     */
    public function createCheckoutSession(
        User $user,
        Plan $plan,
        string $period,
        ?int $currencyId = null,
    ): CheckoutSession;

    /**
     * Cancel a subscription. When `$atPeriodEnd` is true, the user keeps
     * access until `current_period_ends_at`; the local `cancels_at_period_end`
     * flag is set. When false, the cancellation is immediate (rare — used
     * for fraud / refund cases).
     */
    public function cancelSubscription(Subscription $subscription, bool $atPeriodEnd = true): void;

    /**
     * Re-fetch the subscription's CURRENT state from the provider and report
     * whether it is still LIVE — active OR recoverable (e.g. `past_due` in
     * dunning, `suspended`) — rather than trusting the local row's possibly
     * stale status. Returns:
     *   true  — live/recoverable; callers MUST NOT cancel it,
     *   false — definitively terminal on the provider (safe to cancel / sync),
     *   null  — couldn't determine (no provider id / fetch failed); callers
     *           treat null conservatively and do NOT cancel.
     * Used by `subscriptions:cleanup-stale` so a sub the provider's dunning
     * recovered isn't wrongly canceled when its lifecycle webhook was missed.
     */
    public function fetchSubscriptionIsLive(Subscription $subscription): ?bool;

    /**
     * Resume a previously-canceled-at-period-end subscription before its
     * current period closes. After period_ends_at the only path is a fresh
     * checkout.
     */
    public function resumeSubscription(Subscription $subscription): void;

    /**
     * Switch `$subscription` to `$newPlan` at `$newPeriod`. Upgrades prorate
     * immediately (Stripe handles natively; Razorpay applies at next cycle —
     * provider impls document the difference). Downgrades take effect at
     * `current_period_ends_at` and `cancels_at_period_end` is set to true.
     *
     * Returns a redirect URL when the change requires user-side payment
     * action (Stripe 3DS / SCA, Razorpay's cancel-then-create checkout).
     * Returns null when the change applied immediately with no further
     * action needed.
     */
    public function changePlan(
        Subscription $subscription,
        Plan $newPlan,
        string $newPeriod,
        ?int $currencyId = null,
    ): ?string;

    /**
     * Refund a paid invoice. Pass `$amountMinor` (cents/paise) for a partial
     * refund, or null for a full refund of the original invoice amount.
     * Implementations must update the linked provider-side payment + return
     * the provider's refund identifier (Stripe `re_xxx`, Razorpay `rfnd_xxx`)
     * for audit-log persistence; the caller persists `invoices.status='refunded'`.
     *
     * Throws `RuntimeException` on provider rejection (already refunded,
     * dispute, etc.) so the admin UI can surface the underlying message.
     */
    public function refundInvoice(Invoice $invoice, ?int $amountMinor = null): string;

    /**
     * Register or update a webhook endpoint on the provider's side, pointing
     * at our public webhook URL. Auto-generates the signing secret (Razorpay)
     * or captures the provider-generated secret (Stripe). Saves the secret
     * to local config for verification on incoming webhooks.
     *
     * Returns:
     * - `['secret' => string|null, 'provider_webhook_id' => string, 'created' => bool]`
     *   on success. `secret` is null when the existing webhook's secret is
     *   not retrievable (Stripe doesn't expose existing endpoint secrets) —
     *   caller should leave the previously-saved secret in place.
     * - null when the public URL isn't reachable by the provider (localhost,
     *   private IP range). Caller should show a warning + tell admin to
     *   register the webhook manually in the provider dashboard.
     *
     * Throws RuntimeException on API errors (invalid keys, restricted scope,
     * network errors). Caller catches and surfaces to admin UI.
     *
     * @return array{secret: ?string, provider_webhook_id: string, created: bool}|null
     */
    public function ensureWebhookEndpoint(string $publicUrl): ?array;
}
