<?php

declare(strict_types=1);

namespace App\Services;

use RuntimeException;

/**
 * StripeService
 *
 * A deliberately small, dependency-free wrapper over Stripe's REST API
 * (plain cURL, same discipline as TwilioClient/ElevenLabsClient
 * elsewhere in this codebase) rather than pulling in stripe-php — this
 * app only ever needs four calls: create a Customer, start a yearly
 * Checkout Subscription, and verify a webhook signature.
 *
 * Every public method throws RuntimeException immediately if
 * configured() is false, so a half-set-up .env fails loudly at the
 * call site instead of silently doing nothing. Nothing here has been
 * exercised against a real Stripe account yet — do that in test mode
 * before flipping any tenant over to "pay by card" in production.
 *
 * TODO once you have a Stripe account:
 *   1. Create one recurring yearly Price (Product: "Company subscription").
 *   2. Put its price_... id in .env as STRIPE_PRICE_ID.
 *   3. Put your sk_/pk_ keys in .env.
 *   4. In the Stripe Dashboard, add a webhook endpoint pointing at
 *      https://admin.<APP_DOMAIN>/webhooks/stripe listening for at
 *      least: checkout.session.completed, invoice.paid,
 *      invoice.payment_failed, customer.subscription.deleted.
 *      Put its signing secret in .env as STRIPE_WEBHOOK_SECRET.
 */
final class StripeService
{
    private const API_BASE = 'https://api.stripe.com/v1';

    public static function configured(): bool
    {
        return (string) config('stripe.secret_key', '') !== ''
            && (string) config('stripe.yearly_price_id', '') !== '';
    }

    /**
     * Creates a Stripe Checkout Session for a new yearly subscription.
     * The card is collected by Stripe's own hosted page — this app
     * never touches raw card numbers — and Stripe saves it to the
     * Customer automatically for the following year's auto-renewal.
     *
     * @return string the Checkout Session URL to redirect the browser to
     */
    public function createCheckoutSession(int $businessLineId, string $customerEmail, string $successUrl, string $cancelUrl): string
    {
        $this->assertConfigured();

        $response = $this->request('POST', '/checkout/sessions', [
            'mode' => 'subscription',
            'customer_email' => $customerEmail,
            'line_items' => [[
                'price' => (string) config('stripe.yearly_price_id'),
                'quantity' => 1,
            ]],
            'success_url' => $successUrl . '?session_id={CHECKOUT_SESSION_ID}',
            'cancel_url' => $cancelUrl,
            // Round-trips the tenant through Stripe and back so the
            // webhook/success handler knows which business_lines row
            // this checkout belongs to.
            'client_reference_id' => (string) $businessLineId,
            'subscription_data' => [
                'metadata' => ['business_line_id' => (string) $businessLineId],
            ],
        ]);

        return $response['url'];
    }

    /** Verifies a webhook payload actually came from Stripe (constant-time signature check) before it's trusted. */
    public function verifyWebhookSignature(string $rawPayload, string $signatureHeader): bool
    {
        $secret = (string) config('stripe.webhook_secret', '');
        if ($secret === '') {
            return false;
        }

        $parts = [];
        foreach (explode(',', $signatureHeader) as $pair) {
            [$k, $v] = array_pad(explode('=', $pair, 2), 2, '');
            $parts[$k][] = $v;
        }

        $timestamp = $parts['t'][0] ?? null;
        $signatures = $parts['v1'] ?? [];

        if ($timestamp === null || $signatures === []) {
            return false;
        }

        $expected = hash_hmac('sha256', $timestamp . '.' . $rawPayload, $secret);

        foreach ($signatures as $sig) {
            if (hash_equals($expected, $sig)) {
                return true;
            }
        }

        return false;
    }

    private function assertConfigured(): void
    {
        if (!self::configured()) {
            throw new RuntimeException(
                'Stripe is not configured yet — set STRIPE_SECRET_KEY and STRIPE_PRICE_ID in .env. '
                . 'Until then, activate tenants via a subscription code or manual admin activation instead.'
            );
        }
    }

    /** @param array<string, mixed> $params */
    private function request(string $method, string $path, array $params = []): array
    {
        $ch = curl_init(self::API_BASE . $path);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CUSTOMREQUEST => $method,
            CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . (string) config('stripe.secret_key')],
            CURLOPT_POSTFIELDS => http_build_query($this->flatten($params)),
            CURLOPT_TIMEOUT => 15,
        ]);

        $body = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        curl_close($ch);

        if ($body === false) {
            throw new RuntimeException("Stripe request failed: {$error}");
        }

        $decoded = json_decode((string) $body, true);

        if ($status >= 400) {
            $message = $decoded['error']['message'] ?? "Stripe API error (HTTP {$status})";
            throw new RuntimeException($message);
        }

        return is_array($decoded) ? $decoded : [];
    }

    /** Stripe's API expects PHP-style bracketed keys for nested params (line_items[0][price]=...). */
    private function flatten(array $params, string $prefix = ''): array
    {
        $out = [];
        foreach ($params as $key => $value) {
            $fullKey = $prefix === '' ? (string) $key : "{$prefix}[{$key}]";
            if (is_array($value)) {
                $out += $this->flatten($value, $fullKey);
            } else {
                $out[$fullKey] = $value;
            }
        }

        return $out;
    }
}
