<?php

declare(strict_types=1);

require_once __DIR__ . '/bootstrap.php';

/**
 * Minimal Stripe REST API client using PHP streams (no curl extension
 * available in this environment, and streams are more portable anyway -
 * no dependency on a vendored SDK, no composer.json needed for this).
 */

function stripe_secret_key(): string
{
    $key = app_env('STRIPE_SECRET_KEY');
    if ($key === null || trim($key) === '') {
        throw new RuntimeException('STRIPE_SECRET_KEY ist nicht gesetzt.');
    }

    return $key;
}

function stripe_webhook_secret(): string
{
    $secret = app_env('STRIPE_WEBHOOK_SECRET');
    if ($secret === null || trim($secret) === '') {
        throw new RuntimeException('STRIPE_WEBHOOK_SECRET ist nicht gesetzt.');
    }

    return $secret;
}

/**
 * Flattens nested params into Stripe's bracket-notation form fields, e.g.
 * ['line_items' => [['price' => 'x']]] -> line_items[0][price]=x
 *
 * @param array<int|string, mixed> $params
 * @return array<string, string>
 */
function stripe_flatten_params(array $params, string $prefix = ''): array
{
    $flat = [];
    foreach ($params as $key => $value) {
        $paramKey = $prefix === '' ? (string)$key : "{$prefix}[{$key}]";
        if (is_array($value)) {
            $flat += stripe_flatten_params($value, $paramKey);
        } elseif ($value !== null) {
            $flat[$paramKey] = (string)$value;
        }
    }

    return $flat;
}

/**
 * @param array<string, mixed> $params
 * @return array{ok: bool, status: int, data: array, error: ?string}
 */
function stripe_request(string $method, string $path, array $params = []): array
{
    $url = 'https://api.stripe.com/v1/' . ltrim($path, '/');
    $body = null;

    if ($method === 'GET') {
        if ($params !== []) {
            $url .= '?' . http_build_query(stripe_flatten_params($params));
        }
    } else {
        $body = http_build_query(stripe_flatten_params($params));
    }

    $headers = [
        'Authorization: Bearer ' . stripe_secret_key(),
        'Content-Type: application/x-www-form-urlencoded',
    ];

    $context = stream_context_create([
        'http' => [
            'method' => $method,
            'header' => implode("\r\n", $headers),
            'content' => $body,
            'timeout' => 20,
            'ignore_errors' => true,
        ],
    ]);

    $responseBody = @file_get_contents($url, false, $context);
    $status = 0;
    foreach ($http_response_header ?? [] as $header) {
        if (preg_match('~^HTTP/\S+\s+(\d{3})~', $header, $m) === 1) {
            $status = (int)$m[1];
        }
    }

    if ($responseBody === false) {
        return ['ok' => false, 'status' => 0, 'data' => [], 'error' => 'Stripe-Anfrage fehlgeschlagen (keine Antwort).'];
    }

    $data = json_decode($responseBody, true);
    if (!is_array($data)) {
        return ['ok' => false, 'status' => $status, 'data' => [], 'error' => 'Ungültige Antwort von Stripe.'];
    }

    if ($status >= 400) {
        return ['ok' => false, 'status' => $status, 'data' => $data, 'error' => $data['error']['message'] ?? 'Stripe-Fehler.'];
    }

    return ['ok' => true, 'status' => $status, 'data' => $data, 'error' => null];
}

/**
 * Finds an existing recurring Price by lookup_key, or creates the Product
 * and Price if none exists yet. Idempotent: safe to call on every request
 * that needs the price id (result should be cached by the caller).
 */
function stripe_find_or_create_price(string $lookupKey, string $productName, int $priceCents): ?string
{
    $existing = stripe_request('GET', 'prices', ['lookup_keys' => [$lookupKey], 'active' => 'true']);
    if ($existing['ok'] && !empty($existing['data']['data'][0]['id'])) {
        return (string)$existing['data']['data'][0]['id'];
    }

    $product = stripe_request('POST', 'products', ['name' => $productName]);
    if (!$product['ok']) {
        return null;
    }

    $price = stripe_request('POST', 'prices', [
        'product' => $product['data']['id'],
        'unit_amount' => $priceCents,
        'currency' => 'eur',
        'recurring' => ['interval' => 'month'],
        'lookup_key' => $lookupKey,
    ]);

    return $price['ok'] ? (string)$price['data']['id'] : null;
}

/**
 * @return array{ok: bool, url: ?string, error: ?string}
 */
function stripe_create_checkout_session(string $priceId, string $customerEmail, ?string $existingCustomerId, string $successUrl, string $cancelUrl, array $metadata = []): array
{
    $params = [
        'mode' => 'subscription',
        'line_items' => [['price' => $priceId, 'quantity' => 1]],
        'success_url' => $successUrl,
        'cancel_url' => $cancelUrl,
        'metadata' => $metadata,
        // Auch auf die Subscription selbst spiegeln: subscription.updated/
        // .deleted-Events liefern kein Checkout-Session-Objekt, aber die
        // Metadaten der Subscription, darueber loesen wir tenant_id auf.
        'subscription_data' => ['metadata' => $metadata],
    ];

    if ($existingCustomerId !== null) {
        $params['customer'] = $existingCustomerId;
    } else {
        $params['customer_email'] = $customerEmail;
    }

    $result = stripe_request('POST', 'checkout/sessions', $params);
    if (!$result['ok']) {
        return ['ok' => false, 'url' => null, 'error' => $result['error']];
    }

    return ['ok' => true, 'url' => (string)$result['data']['url'], 'error' => null];
}

/**
 * @return array{ok: bool, url: ?string, error: ?string}
 */
function stripe_create_billing_portal_session(string $customerId, string $returnUrl): array
{
    $result = stripe_request('POST', 'billing_portal/sessions', [
        'customer' => $customerId,
        'return_url' => $returnUrl,
    ]);
    if (!$result['ok']) {
        return ['ok' => false, 'url' => null, 'error' => $result['error']];
    }

    return ['ok' => true, 'url' => (string)$result['data']['url'], 'error' => null];
}

/**
 * Verifies a Stripe webhook signature per Stripe's documented scheme:
 * the Stripe-Signature header contains "t=<timestamp>,v1=<hmac>[,v0=...]";
 * the expected signature is HMAC-SHA256("{t}.{payload}", secret). A
 * $toleranceSeconds window guards against replay of old, captured payloads.
 */
function stripe_verify_webhook_signature(string $payload, string $signatureHeader, string $secret, int $toleranceSeconds = 300): bool
{
    $parts = [];
    foreach (explode(',', $signatureHeader) as $piece) {
        [$k, $v] = array_pad(explode('=', trim($piece), 2), 2, null);
        if ($k !== null && $v !== null) {
            $parts[$k][] = $v;
        }
    }

    $timestamp = isset($parts['t'][0]) ? (int)$parts['t'][0] : 0;
    $signatures = $parts['v1'] ?? [];
    if ($timestamp <= 0 || $signatures === []) {
        return false;
    }

    if (abs(time() - $timestamp) > $toleranceSeconds) {
        return false;
    }

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

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

    return false;
}

/**
 * @return array{id: string, type: string, data: array}|null
 */
function stripe_parse_event(string $payload): ?array
{
    $decoded = json_decode($payload, true);
    if (!is_array($decoded) || !isset($decoded['type'], $decoded['data']['object'])) {
        return null;
    }

    return [
        'id' => (string)($decoded['id'] ?? ''),
        'type' => (string)$decoded['type'],
        'data' => $decoded['data']['object'],
    ];
}
