<?php
declare(strict_types=1);

require_once __DIR__ . '/db.php';

interface PaymentGateway
{
    public function providerId(): string;

    /**
     * @param array{
     *   amount:float,currency:string,description:string,customer_email?:?string,
     *   success_url:string,cancel_url:string,metadata?:array
     * } $input
     * @return array{external_id:string,checkout_url:?string,status:string,raw?:array}
     */
    public function createCheckout(array $input): array;

    /**
     * @return array{status:string,external_id:?string,raw?:array}
     */
    public function fetchStatus(string $externalId): array;
}

function payment_settings(int $tenantId): array
{
    $stmt = db()->prepare('SELECT * FROM payment_settings WHERE tenant_id = ?');
    $stmt->execute([$tenantId]);
    $row = $stmt->fetch();
    if ($row) {
        return $row;
    }
    db()->prepare(
        'INSERT INTO payment_settings (tenant_id, mode, enable_pay_at_shop, retail_payments, booking_payments)
         VALUES (?, \'test\', 1, \'optional\', \'off\')'
    )->execute([$tenantId]);
    $stmt->execute([$tenantId]);
    return $stmt->fetch() ?: [
        'tenant_id' => $tenantId,
        'mode' => 'test',
        'enable_stripe' => 0,
        'enable_paypal' => 0,
        'enable_venmo' => 0,
        'enable_square' => 0,
        'enable_ach' => 0,
        'enable_pay_at_shop' => 1,
        'default_currency' => 'USD',
        'booking_payments' => 'off',
        'retail_payments' => 'optional',
    ];
}

/**
 * True for the public demo package (local or demo.barber-shop.us).
 */
function payment_is_demo_environment(): bool
{
    if (defined('DEMO_MODE') && DEMO_MODE) {
        return true;
    }
    if (defined('APP_BASE') && str_contains((string) APP_BASE, 'demo')) {
        return true;
    }
    return false;
}

/**
 * @return list<array{id:string,label:string,hint:string}>
 */
function payment_available_methods(int $tenantId, string $context = 'retail'): array
{
    $s = payment_settings($tenantId);
    $methods = [];
    $mode = $context === 'booking' ? (string) $s['booking_payments'] : (string) $s['retail_payments'];
    $manualLabel = $context === 'booking' ? 'Pay at the chair' : 'Pay at shop';
    $manualHint = $context === 'booking'
        ? 'Pay when you arrive — no card needed online'
        : 'Pay when you pick up or arrive';

    // Demo booking always offers chair + pay-in-advance (mock checkout).
    if ($context === 'booking' && payment_is_demo_environment()) {
        $methods[] = ['id' => 'manual', 'label' => $manualLabel, 'hint' => $manualHint];
        $methods[] = [
            'id' => 'demo',
            'label' => 'Pay in advance',
            'hint' => 'Card / bank · demo checkout (no real charge)',
        ];
        return $methods;
    }

    // Booking with online pay disabled: always offer pay-at-chair (do not hide behind a flag).
    if ($mode === 'off') {
        if ($context === 'booking' || !empty($s['enable_pay_at_shop'])) {
            $methods[] = ['id' => 'manual', 'label' => $manualLabel, 'hint' => $manualHint];
        }
        return $methods;
    }

    if (!empty($s['enable_stripe']) && !empty($s['stripe_secret_key'])) {
        $methods[] = ['id' => 'stripe', 'label' => 'Card (Stripe)', 'hint' => 'Visa, Mastercard, Amex, and more'];
    }
    if (!empty($s['enable_paypal']) && !empty($s['paypal_client_id']) && !empty($s['paypal_client_secret'])) {
        $label = !empty($s['enable_venmo']) ? 'PayPal / Venmo' : 'PayPal';
        $methods[] = ['id' => 'paypal', 'label' => $label, 'hint' => 'PayPal balance, linked bank, or Venmo (US)'];
    }
    if (!empty($s['enable_square']) && !empty($s['square_access_token'])) {
        $methods[] = ['id' => 'square', 'label' => 'Card (Square)', 'hint' => 'Coming online soon'];
    }
    $requireOnline = in_array($mode, ['online', 'deposit', 'full'], true);
    if (!$requireOnline && ($context === 'booking' || !empty($s['enable_pay_at_shop']))) {
        $methods[] = ['id' => 'manual', 'label' => $manualLabel, 'hint' => $manualHint];
    }
    // If online was requested but nothing is configured, fall back so booking still works.
    if ($context === 'booking' && !$methods) {
        $methods[] = ['id' => 'manual', 'label' => $manualLabel, 'hint' => $manualHint];
    }
    $seen = [];
    $out = [];
    foreach ($methods as $m) {
        if (isset($seen[$m['id']])) {
            continue;
        }
        $seen[$m['id']] = true;
        $out[] = $m;
    }
    return $out;
}

function payment_gateway(int $tenantId, string $provider): PaymentGateway
{
    $s = payment_settings($tenantId);
    require_once __DIR__ . '/payments/ManualGateway.php';
    require_once __DIR__ . '/payments/StripeGateway.php';
    require_once __DIR__ . '/payments/PayPalGateway.php';
    require_once __DIR__ . '/payments/SquareGateway.php';

    return match ($provider) {
        'stripe' => new StripeGateway($s),
        'paypal' => new PayPalGateway($s),
        'square' => new SquareGateway($s),
        'manual' => new ManualGateway($s),
        default => throw new InvalidArgumentException('Unknown payment provider: ' . $provider),
    };
}

function payment_create_record(
    int $tenantId,
    string $provider,
    string $purpose,
    string $referenceType,
    int $referenceId,
    float $amount,
    string $currency,
    array $checkoutResult
): int {
    db()->prepare(
        'INSERT INTO payments (tenant_id, provider, purpose, reference_type, reference_id, amount, currency, status, external_id, checkout_url, meta_json, paid_at)
         VALUES (?,?,?,?,?,?,?,?,?,?,?,?)'
    )->execute([
        $tenantId,
        $provider,
        $purpose,
        $referenceType,
        $referenceId,
        $amount,
        strtoupper($currency),
        $checkoutResult['status'] ?? 'pending',
        $checkoutResult['external_id'] ?? null,
        $checkoutResult['checkout_url'] ?? null,
        isset($checkoutResult['raw']) ? json_encode($checkoutResult['raw'], JSON_UNESCAPED_SLASHES) : null,
        ($checkoutResult['status'] ?? '') === 'paid' ? date('Y-m-d H:i:s') : null,
    ]);
    return (int) db()->lastInsertId();
}

function payment_mark_paid(int $paymentId, ?string $externalId = null, ?array $raw = null): void
{
    $stmt = db()->prepare('SELECT * FROM payments WHERE id = ?');
    $stmt->execute([$paymentId]);
    $pay = $stmt->fetch();
    if (!$pay || $pay['status'] === 'paid') {
        if ($pay && $raw) {
            db()->prepare('UPDATE payments SET meta_json = ? WHERE id = ?')->execute([
                json_encode($raw, JSON_UNESCAPED_SLASHES),
                $paymentId,
            ]);
        }
        return;
    }
    db()->prepare(
        'UPDATE payments SET status = ?, paid_at = NOW(), external_id = COALESCE(?, external_id), meta_json = COALESCE(?, meta_json) WHERE id = ?'
    )->execute([
        'paid',
        $externalId,
        $raw ? json_encode($raw, JSON_UNESCAPED_SLASHES) : null,
        $paymentId,
    ]);
    payment_apply_reference_paid($pay);
}

function payment_apply_reference_paid(array $pay): void
{
    if ($pay['reference_type'] === 'shop_order') {
        db()->prepare(
            'UPDATE shop_orders SET status = ?, payment_status = ?, payment_provider = ? WHERE id = ? AND tenant_id = ?'
        )->execute([
            'pending_pickup',
            'paid',
            $pay['provider'],
            (int) $pay['reference_id'],
            (int) $pay['tenant_id'],
        ]);
    }
    if ($pay['reference_type'] === 'booking') {
        $status = in_array((string) ($pay['purpose'] ?? ''), ['cancel_fee', 'no_show'], true)
            ? null
            : 'confirmed';
        if ($status) {
            db()->prepare(
                'UPDATE bookings SET payment_status = ?, deposit_paid = deposit_paid + ?, status = ? WHERE id = ? AND tenant_id = ?'
            )->execute([
                'paid',
                (float) $pay['amount'],
                $status,
                (int) $pay['reference_id'],
                (int) $pay['tenant_id'],
            ]);
        } else {
            db()->prepare(
                'UPDATE bookings SET payment_status = ?, deposit_paid = deposit_paid + ? WHERE id = ? AND tenant_id = ?'
            )->execute([
                'fee_paid',
                (float) $pay['amount'],
                (int) $pay['reference_id'],
                (int) $pay['tenant_id'],
            ]);
        }
    }
}

function payment_find_by_external(string $provider, string $externalId): ?array
{
    $stmt = db()->prepare('SELECT * FROM payments WHERE provider = ? AND external_id = ? LIMIT 1');
    $stmt->execute([$provider, $externalId]);
    $row = $stmt->fetch();
    return $row ?: null;
}

function payment_mask_secret(?string $value): string
{
    $value = (string) $value;
    if ($value === '') {
        return '';
    }
    if (strlen($value) <= 8) {
        return str_repeat('•', strlen($value));
    }
    return substr($value, 0, 4) . str_repeat('•', max(4, strlen($value) - 8)) . substr($value, -4);
}

/**
 * Shared HTTP helper for provider APIs.
 *
 * @param array<string,string> $headers
 * @return array{status:int,body:string,json:?array}
 */
function payment_http(string $method, string $url, array $headers = [], ?string $body = null): array
{
    $ch = curl_init($url);
    $hdrs = $headers;
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST => strtoupper($method),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => $hdrs,
        CURLOPT_TIMEOUT => 30,
    ]);
    if ($body !== null) {
        curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
    }
    $resp = curl_exec($ch);
    $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
    if ($resp === false) {
        $err = curl_error($ch);
        curl_close($ch);
        throw new RuntimeException('Payment HTTP error: ' . $err);
    }
    curl_close($ch);
    $json = json_decode($resp, true);
    return ['status' => $status, 'body' => $resp, 'json' => is_array($json) ? $json : null];
}

function payment_service_deposit_amount(array $service): float
{
    $mode = (string) ($service['deposit_mode'] ?? 'none');
    $value = (float) ($service['deposit_value'] ?? 0);
    $price = (float) ($service['price'] ?? 0);
    return match ($mode) {
        'fixed' => max(0, round($value, 2)),
        'percent' => max(0, round($price * ($value / 100), 2)),
        'full' => max(0, round($price, 2)),
        default => 0.0,
    };
}

/**
 * How much to collect online for a booking, given shop mode + service deposit rules.
 *
 * @return array{required:bool,optional:bool,amount:float,purpose:string,label:string,currency:string}
 */
function payment_booking_due(int $tenantId, array $service): array
{
    $s = payment_settings($tenantId);
    $currency = strtoupper((string) ($s['default_currency'] ?? 'USD'));
    $mode = (string) ($s['booking_payments'] ?? 'off');
    $price = max(0, round((float) ($service['price'] ?? 0), 2));
    $deposit = payment_service_deposit_amount($service);

    if ($mode === 'off') {
        if (payment_is_demo_environment()) {
            // Demo still shows pay-in-advance amount even when shop mode is "off".
            return [
                'required' => false,
                'optional' => true,
                'amount' => $price,
                'purpose' => 'booking_full',
                'label' => 'Choose how to pay',
                'currency' => $currency,
            ];
        }
        return [
            'required' => false,
            'optional' => false,
            'amount' => 0.0,
            'purpose' => 'none',
            'label' => 'Pay at the chair',
            'currency' => $currency,
        ];
    }

    if ($mode === 'full') {
        return [
            'required' => true,
            'optional' => false,
            'amount' => $price,
            'purpose' => 'booking_full',
            'label' => 'Pay in full online',
            'currency' => $currency,
        ];
    }

    if ($mode === 'deposit') {
        $amount = $deposit > 0 ? $deposit : $price;
        return [
            'required' => $amount > 0,
            'optional' => false,
            'amount' => $amount,
            'purpose' => $deposit > 0 && $deposit < $price ? 'booking_deposit' : 'booking_full',
            'label' => $deposit > 0 && $deposit < $price
                ? 'Deposit due online ($' . number_format($amount, 2) . ')'
                : 'Pay in full online',
            'currency' => $currency,
        ];
    }

    // optional
    $amount = $deposit > 0 ? $deposit : $price;
    return [
        'required' => false,
        'optional' => true,
        'amount' => $amount,
        'purpose' => $deposit > 0 && $deposit < $price ? 'booking_deposit' : 'booking_full',
        'label' => 'Optional online pay',
        'currency' => $currency,
    ];
}

/**
 * Start Stripe/PayPal checkout for an existing booking or order reference.
 *
 * @return array{payment_id:int,checkout_url:?string,status:string}
 */
function payment_start_checkout(
    int $tenantId,
    string $provider,
    string $purpose,
    string $referenceType,
    int $referenceId,
    float $amount,
    string $currency,
    string $description,
    ?string $customerEmail,
    string $tenantParam
): array {
    if ($amount <= 0 && $provider !== 'manual') {
        throw new RuntimeException('Amount must be greater than zero.');
    }
    $gw = payment_gateway($tenantId, $provider);
    $baseReturn = app_origin() . APP_BASE . '/payments/return.php?tenant=' . urlencode($tenantParam);

    if ($provider === 'manual') {
        $result = $gw->createCheckout([
            'amount' => $amount,
            'currency' => $currency,
            'description' => $description,
            'customer_email' => $customerEmail,
            'success_url' => $baseReturn,
            'cancel_url' => $baseReturn . '&cancel=1',
            'metadata' => ['reference_id' => (string) $referenceId],
        ]);
        $paymentId = payment_create_record(
            $tenantId,
            $provider,
            $purpose,
            $referenceType,
            $referenceId,
            $amount,
            $currency,
            $result
        );
        return ['payment_id' => $paymentId, 'checkout_url' => null, 'status' => 'paid'];
    }

    $paymentId = payment_create_record(
        $tenantId,
        $provider,
        $purpose,
        $referenceType,
        $referenceId,
        $amount,
        $currency,
        ['status' => 'pending', 'external_id' => null, 'checkout_url' => null]
    );
    $successUrl = $baseReturn . '&payment_id=' . $paymentId;
    $cancelUrl = $successUrl . '&cancel=1';
    if ($provider === 'stripe') {
        $successUrl .= '&session_id={CHECKOUT_SESSION_ID}';
    }
    $result = $gw->createCheckout([
        'amount' => $amount,
        'currency' => $currency,
        'description' => $description,
        'customer_email' => $customerEmail,
        'success_url' => $successUrl,
        'cancel_url' => $cancelUrl,
        'metadata' => [
            'reference_type' => $referenceType,
            'reference_id' => (string) $referenceId,
            'tenant_id' => (string) $tenantId,
            'payment_id' => (string) $paymentId,
            'purpose' => $purpose,
        ],
    ]);
    db()->prepare(
        'UPDATE payments SET external_id = ?, checkout_url = ?, status = ?, meta_json = ? WHERE id = ? AND tenant_id = ?'
    )->execute([
        $result['external_id'] ?? null,
        $result['checkout_url'] ?? null,
        $result['status'] ?? 'pending',
        isset($result['raw']) ? json_encode($result['raw'], JSON_UNESCAPED_SLASHES) : null,
        $paymentId,
        $tenantId,
    ]);
    if (empty($result['checkout_url'])) {
        throw new RuntimeException('Payment provider did not return a checkout URL. Check API keys.');
    }
    return [
        'payment_id' => $paymentId,
        'checkout_url' => (string) $result['checkout_url'],
        'status' => (string) ($result['status'] ?? 'pending'),
    ];
}

function payment_mark_refunded(int $paymentId, int $tenantId, string $note = ''): void
{
    $stmt = db()->prepare('SELECT * FROM payments WHERE id = ? AND tenant_id = ?');
    $stmt->execute([$paymentId, $tenantId]);
    $pay = $stmt->fetch();
    if (!$pay) {
        throw new RuntimeException('Payment not found.');
    }
    if ($pay['status'] === 'refunded') {
        return;
    }

    if ($pay['provider'] === 'stripe' && !empty($pay['external_id'])) {
        $s = payment_settings($tenantId);
        $secret = trim((string) ($s['stripe_secret_key'] ?? ''));
        if ($secret !== '') {
            $sess = payment_http(
                'GET',
                'https://api.stripe.com/v1/checkout/sessions/' . rawurlencode((string) $pay['external_id']) . '?expand[]=payment_intent',
                ['Authorization: Bearer ' . $secret]
            );
            $pi = $sess['json']['payment_intent'] ?? null;
            $piId = is_array($pi) ? ($pi['id'] ?? null) : $pi;
            if ($piId) {
                $ref = payment_http(
                    'POST',
                    'https://api.stripe.com/v1/refunds',
                    [
                        'Authorization: Bearer ' . $secret,
                        'Content-Type: application/x-www-form-urlencoded',
                    ],
                    http_build_query(['payment_intent' => $piId])
                );
                if ($ref['status'] >= 400) {
                    $msg = $ref['json']['error']['message'] ?? 'Stripe refund failed';
                    throw new RuntimeException($msg);
                }
            }
        }
    }

    $meta = json_decode((string) ($pay['meta_json'] ?? '{}'), true) ?: [];
    $meta['refund_note'] = $note;
    $meta['refunded_at'] = date('c');
    db()->prepare('UPDATE payments SET status = ?, meta_json = ? WHERE id = ?')->execute([
        'refunded',
        json_encode($meta, JSON_UNESCAPED_SLASHES),
        $paymentId,
    ]);
    if ($pay['reference_type'] === 'booking') {
        db()->prepare(
            'UPDATE bookings SET payment_status = ? WHERE id = ? AND tenant_id = ?'
        )->execute(['refunded', (int) $pay['reference_id'], $tenantId]);
    }
    if ($pay['reference_type'] === 'shop_order') {
        db()->prepare(
            'UPDATE shop_orders SET payment_status = ? WHERE id = ? AND tenant_id = ?'
        )->execute(['refunded', (int) $pay['reference_id'], $tenantId]);
    }
}
