<?php

namespace App\Services\Stripe;

/**
 * The bit of Stripe we drive ourselves: the catalogue.
 *
 * Everything else about recurring billing — retries, dunning, off-session SCA,
 * invoice numbering — is Stripe's job, and we learn about it through webhooks.
 * Our side owns capability, because Stripe does not know how big the VM should
 * be.
 */
interface StripeClient
{
    /**
     * Settle an upgrade at once: Stripe works out the days left in the term,
     * raises its own invoice for the difference and charges it there and then.
     *
     * Chosen over letting the proration ride on the next cycle invoice because
     * the cycle invoice is the one WE issue a document for, from the contract's
     * frozen price. A renewal invoice that said one figure while Stripe took
     * that figure plus a proration would be a document that does not match the
     * money — see App\Actions\MoveStripeSubscriptionPrice.
     */
    public const PRORATE_IMMEDIATELY = 'always_invoice';

    /**
     * Change the price and settle nothing. What the next cycle is billed at
     * changes; no credit and no charge is raised for the switch itself.
     */
    public const PRORATE_NONE = 'none';

    /**
     * Stop at the end of the term the customer has already paid for.
     *
     * What a CANCELLATION means. They keep the service to the boundary they have
     * been billed to, no further cycle is ever raised, and Stripe ends the
     * subscription itself at that moment — which arrives here as
     * `customer.subscription.deleted` and is what ApplyStripeBillingEvent reads
     * as the end of the contract.
     */
    public const CANCEL_AT_PERIOD_END = 'at_period_end';

    /**
     * Stop now.
     *
     * What a WITHDRAWAL means. The contract is unwound rather than ended, the
     * whole amount goes back, and there is no paid-up term left to run out — see
     * App\Actions\WithdrawContract.
     */
    public const CANCEL_IMMEDIATELY = 'immediately';

    /** Whether an account is actually configured. */
    public function isConfigured(): bool;

    /**
     * Open a hosted checkout for one plan Price and return the URL to send the
     * customer to.
     *
     * Hosted, not a payment form of ours: card details, 3-D Secure and every
     * local payment method are Stripe's problem, and the moment a page of ours
     * touches a card number the whole installation is in PCI scope.
     *
     * `$metadata` is what comes back on the webhook and decides what is built —
     * plan, plan version and datacenter. It is set on the SESSION and on the
     * subscription: the session's copy is what StripeWebhookController reads,
     * the subscription's is what any later billing event carries.
     *
     * `$priceId` is one of the TWO Prices the catalogue sells that package on —
     * the domestic gross or the bare net a reverse-charge business owes — and which
     * one it is has been decided by TaxTreatment before this is called. Nothing
     * here computes tax, and `automatic_tax` is deliberately never enabled: a
     * second rate worked out by Stripe would charge a German consumer 19 % while
     * our own document said 20 %.
     *
     * `$oneOff` is the setup fee: a second line item with no recurrence, whose
     * `amount_cents` is what this same customer is charged for it, formed the same
     * way and by the same authority as the Price above.
     * Stripe allows one-time prices in a subscription-mode session and puts them
     * on the INITIAL invoice only, which is precisely what a setup fee is — so it
     * is charged once, with the first month, and never again. Null means there is
     * no such fee and no second line is sent at all: a zero-amount line on a
     * checkout is a line telling the customer they owe nothing for something.
     *
     * `label` is what that line is CALLED on Stripe's own page and on the receipt
     * Stripe sends. It arrives from the caller rather than being written here
     * because it is customer-facing wording, and that belongs in the language
     * files with the rest of it. `currency` has to match the recurring Price's, so
     * it comes from the same catalogue the Price was formed from.
     *
     * @param  array{amount_cents: int, label: string, currency: string}|null  $oneOff
     */
    public function createCheckoutSession(
        string $priceId,
        string $successUrl,
        string $cancelUrl,
        array $metadata = [],
        ?string $customerEmail = null,
        ?string $idempotencyKey = null,
        ?array $oneOff = null,
    ): string;

    /**
     * Create a Product for a plan family; returns its Stripe id.
     *
     * `$idempotencyKey` is Stripe's own guard: a crash between their creating
     * the object and our storing the id would otherwise mint a second one on
     * the next run, and there is no way to tell duplicates apart afterwards.
     */
    public function createProduct(string $name, array $metadata = [], ?string $idempotencyKey = null): string;

    /**
     * Create a Price. Stripe Prices are immutable and carry their own interval,
     * which is why one exists per plan version AND term — and, since a
     * reverse-charge business is charged the bare net while everybody else pays
     * the gross, two per priced row on each term.
     */
    public function createPrice(
        string $productId,
        int $amountCents,
        string $currency,
        string $interval,
        array $metadata = [],
        ?string $idempotencyKey = null,
    ): string;

    /**
     * Every active recurring Price of a Product, as Stripe holds them, with the
     * properties that decide what each one CHARGES.
     *
     * `interval_count` multiplies the period — 3 bills every three months while
     * `interval` still reads 'month'. `usage_type` other than 'licensed' bills
     * metered usage. `transform_quantity` divides the quantity before charging,
     * and modules are billed BY quantity, so a divide_by of 10 would charge a
     * customer holding three for one. `billing_scheme` 'tiered' keeps the money
     * in tiers and leaves `unit_amount` null, which reads as 0 here.
     *
     * All four are REPORTED, not filtered. Which of them is acceptable is the
     * caller's decision, and the caller is App\Services\Billing\AdoptStripePrice
     * — the class whose docblock promises that adoption cannot move money. That
     * promise has to be testable in the class that makes it, and it was not
     * while these fields were dropped here: FakeStripeClient cannot express
     * them.
     *
     * An absent key is Stripe's own default — 1, 'licensed', none, 'per_unit'.
     *
     * Active ones only, because the question being asked is "is there something
     * here I can sell on?". `currency` comes back UPPER CASE, the way our own
     * tables hold it.
     *
     * @return array<int, array{id: string, unit_amount: int, currency: string, interval: string, interval_count: int, usage_type: string, transform_quantity: bool, billing_scheme: string, created: int, metadata: array<string, string>}>
     */
    public function activePricesFor(string $productId): array;

    /**
     * Every active Product on the account, with its metadata.
     *
     * The other half of the orphan question, and the more consequential one. A
     * run that created a Product and died before storing its id leaves an orphan
     * exactly as a Price does — but a second Product is worse than a duplicate
     * Price, because activePricesFor() is then asked about the wrong one and the
     * Price recognition goes blind for that whole family.
     *
     * The WHOLE active list, with no metadata filter: Stripe cannot search
     * metadata, and an account holds a handful of Products. Which of them is
     * ours is App\Services\Billing\AdoptStripeProduct's decision.
     *
     * @return array<int, array{id: string, name: string, created: int, metadata: array<string, string>}>
     */
    public function activeProducts(): array;

    /**
     * Write metadata onto a Price that already exists.
     *
     * Metadata is one of the few fields a Stripe Price lets you change — the
     * same property activatePrice() rests on, and the reason the metadata format
     * is NOT part of a Price's identity. An adopted orphan is brought up to
     * today's metadata rather than replaced, which is exactly what was done by
     * hand after 2026-07-29 and keeps Stripe's own dashboard readable.
     */
    public function updatePriceMetadata(string $priceId, array $metadata): void;

    /** Stop a Price being offered. The Price itself stays, as Stripe requires. */
    public function archivePrice(string $priceId): void;

    /**
     * Offer an archived Price again.
     *
     * The other half of archivePrice(), and it was missing. A rate that moves and
     * then moves back finds the Price for the figure it has come back to already
     * in our register: the sync brings that row back rather than minting a second
     * Price for one amount, which is right — but the Price was archived AT STRIPE,
     * and only this makes it sellable again. Without it the catalogue pointed at
     * an inactive Price and the failure was a customer meeting a refused checkout.
     */
    public function activatePrice(string $priceId): void;

    /**
     * Move a subscription onto another Price.
     *
     * The ITEM is what carries a price, not the subscription, so the swap needs
     * both ids. `$prorationBehaviour` is one of the constants above and is
     * always passed explicitly: Stripe's own default settles a proration
     * silently, and a default deciding what a customer is charged is exactly
     * the kind of thing that must be written down at the call site.
     */
    public function updateSubscriptionPrice(
        string $subscriptionId,
        string $itemId,
        string $priceId,
        string $prorationBehaviour,
    ): void;

    /**
     * The id of the item a subscription bills its PACKAGE through, or null if it
     * has none.
     *
     * For contracts opened before the item id was stored: Stripe knows it, we
     * did not ask, and asking once is cheaper than being unable to move them.
     * The first item is the package, because that is the one the checkout
     * created; modules are added afterwards and carry their own ids, which are
     * stored on the bookings themselves.
     */
    public function subscriptionItemId(string $subscriptionId): ?string;

    /**
     * Put a module on the subscription as an item of its own, and return the
     * item's id.
     *
     * `$prorationBehaviour` is passed explicitly for the same reason as above.
     * A module booked in the middle of a term is charged PRORATE_IMMEDIATELY:
     * Stripe works out the days that are left, raises an invoice for exactly
     * that and takes the money there and then — which is the invoice the
     * customer is owed a document for.
     */
    public function addSubscriptionItem(
        string $subscriptionId,
        string $priceId,
        int $quantity,
        string $prorationBehaviour,
        ?string $idempotencyKey = null,
    ): string;

    /**
     * Change how many of a module the subscription bills for.
     *
     * A pack is sold by the unit, so a second one is a quantity of two on one
     * item rather than a second item at the same price — two items on one price
     * is a thing Stripe allows and nothing could later tell apart.
     */
    public function updateSubscriptionItemQuantity(
        string $itemId,
        int $quantity,
        string $prorationBehaviour,
    ): void;

    /**
     * Take a module off the subscription.
     *
     * Removed with PRORATE_NONE by everything that calls it: a cancelled module
     * is kept until the end of the term the customer has already paid for and
     * earns no credit, so nothing is to be settled — only the next cycle is
     * smaller.
     */
    public function removeSubscriptionItem(string $itemId, string $prorationBehaviour): void;

    /**
     * Tell Stripe to stop charging for a contract that is over.
     *
     * The one call this platform could not make. Nothing here cancelled a Stripe
     * subscription at all: a customer's cancellation wrote a date on the
     * instance, a withdrawal marked the contract cancelled and sent the money
     * back, and in both cases Stripe went on taking the next month, and the next,
     * for a service that had ended — and every one of those payments arrived as
     * `invoice.paid` and drew a number out of the gapless Austrian series.
     *
     * `$when` is one of the CANCEL_* constants and is always passed explicitly,
     * for the same reason `$prorationBehaviour` is above: the two are different
     * promises to the customer, and which one a caller means has to be legible at
     * the call site rather than inherited from a default written down here.
     *
     * `$idempotencyKey` because a retry after a timeout cannot tell whether the
     * first attempt landed. Cancelling twice is harmless at Stripe, but the same
     * key makes the retry provably a no-op instead of merely a likely one.
     */
    public function cancelSubscription(
        string $subscriptionId,
        string $when,
        ?string $idempotencyKey = null,
    ): void;

    /**
     * Send money back, against the payment that took it.
     *
     * The only outgoing money movement this platform makes, and it exists
     * because a consumer who withdraws within fourteen days is owed one — see
     * App\Actions\WithdrawContract. `$amountCents` is the whole of what was
     * taken: the owner has decided a withdrawing consumer gets everything back,
     * not the amount less the days the cloud ran. It stays an explicit figure
     * rather than "refund the payment" because a Storno states an amount and the
     * money has to be the amount the Storno states.
     *
     * `$paymentReference` is a PaymentIntent (`pi_…`) or a Charge (`ch_…`) —
     * whichever Stripe reported for the payment. Both are accepted rather than
     * one normalised: which of the two an account produces depends on the API
     * version it is pinned to, and a refund that silently does nothing because
     * the wrong field was read is the worst possible failure here.
     *
     * `$idempotencyKey` is not optional in practice. A refund is the one call
     * whose retry after a timeout would send the customer's money twice.
     *
     * @return string the refund's id
     */
    public function refund(
        string $paymentReference,
        ?int $amountCents = null,
        ?string $idempotencyKey = null,
    ): string;

    /**
     * The payment behind a Stripe invoice, or null if it has none.
     *
     * A subscription checkout charges through an invoice, so the invoice id is
     * usually all we have written down — and a refund cannot be issued against
     * an invoice. This is the one hop between the two.
     */
    public function invoicePaymentReference(string $invoiceId): ?string;

    /**
     * Every line on a Stripe invoice, for the document we owe it.
     *
     * The webhook payload carries the lines already; this is for the invoice
     * that has more of them than Stripe puts in an event. A document missing a
     * line that was charged is worse than one nobody sent, so the complete list
     * is worth an extra call.
     *
     * @return array<int, array<string, mixed>>
     */
    public function invoiceLines(string $invoiceId): array;

    /**
     * Ein SetupIntent, mit dem der Browser eine Karte hinterlegt.
     *
     * `usage: off_session`, weil die nächste Abbuchung ohne den Kunden läuft:
     * ohne dieses Feld verlangt Stripe bei jeder Verlängerung eine Freigabe,
     * die zu dem Zeitpunkt niemand geben kann.
     *
     * @return array{id: string, client_secret: string}
     */
    public function createSetupIntent(string $customerId): array;

    /**
     * Das Zahlungsmittel, mit dem künftig abgebucht wird.
     *
     * Eine Karte zu hinterlegen genügt nicht — ohne diesen Schritt bucht
     * Stripe weiter mit der alten ab.
     */
    public function setDefaultPaymentMethod(string $customerId, string $paymentMethodId): void;

    /**
     * Die hinterlegte Karte, oder null.
     *
     * @return array{id: string, brand: string, last4: string, exp_month: int, exp_year: int}|null
     */
    public function defaultPaymentMethod(string $customerId): ?array;

    /**
     * Die finalisierten, unbezahlten Rechnungen dieses Kunden.
     *
     * @return array<int, array{id: string, number: ?string, amount_due_cents: int, currency: string, created_at: string}>
     */
    public function openInvoices(string $customerId): array;

    /**
     * Eine offene Rechnung mit dem hinterlegten Zahlungsmittel einziehen.
     *
     * Wirft NICHT bei einer abgelehnten Karte: das ist die häufigste Antwort
     * überhaupt und gehört auf die Seite, nicht in einen 500er.
     *
     * @return array{paid: bool, status: string, failure: ?string}
     */
    public function payInvoice(string $invoiceId): array;

    /**
     * Eine einmalige Rechnung über einen Betrag, die Stripe selbst einzieht.
     *
     * Für Mahngebühren: eine gescheiterte Abo-Rechnung ist bereits finalisiert
     * und nimmt keinen Posten mehr auf. Die Gebühr wird deshalb eine eigene
     * Rechnung daneben — keine Stornierung, keine Rechnungsnummer wird
     * angefasst, und jede Mahnstufe kann ihre eigene bekommen.
     *
     * @return string die Stripe-ID der Rechnung
     */
    public function createFeeInvoice(
        string $customerId,
        int $amountCents,
        string $currency,
        string $description,
        ?string $idempotencyKey = null,
    ): string;
}
