<?php

namespace App\Http\Controllers\Webhooks;

use App\Http\Controllers\Controller;
use App\Models\Payment;
use App\Models\PaymentGateway;
use App\Models\Subscription;
use App\Services\Payment\PaymentGatewayManager;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;

/**
 * PayPal webhook receiver.
 *
 * POST /paypal/webhook   (registered in routes/web.php — excluded from
 *                         CSRF; signature is the auth).
 *
 * Handles refund, capture, and subscription lifecycle events so the
 * MailTrixy admin panel stays in sync with whatever the seller does
 * inside the PayPal Dashboard (or what PayPal does on its own — e.g.
 * a customer-initiated dispute that ends in a refund).
 *
 * Pattern mirrors StripeWebhookController:
 *   1. 24h idempotency cache by event id
 *   2. Signature verification via PayPalDriver::verifyWebhookSignature()
 *   3. Per-event-type handler with broad try/catch
 *   4. Always return 200 to PayPal unless the signature is bad — failed
 *      handlers are logged but not retried by PayPal (avoids storms).
 *
 * Webhook URL to paste in PayPal Developer Dashboard:
 *   https://your-domain.com/paypal/webhook
 *
 * Events to subscribe to in the PayPal Dashboard:
 *   - PAYMENT.CAPTURE.COMPLETED
 *   - PAYMENT.CAPTURE.REFUNDED
 *   - PAYMENT.CAPTURE.REVERSED
 *   - PAYMENT.CAPTURE.DENIED
 *   - BILLING.SUBSCRIPTION.ACTIVATED
 *   - BILLING.SUBSCRIPTION.CANCELLED
 *   - BILLING.SUBSCRIPTION.SUSPENDED
 *   - BILLING.SUBSCRIPTION.PAYMENT.FAILED
 */
class PayPalWebhookController extends Controller
{
    public function __construct(
        private readonly PaymentGatewayManager $manager,
    ) {}

    public function handle(Request $request): Response
    {
        $rawBody = $request->getContent();
        $headers = $this->normalizeHeaders($request->headers->all());

        // PayPal might not be configured on this install — in which case
        // there's nothing to verify against, so reject cleanly.
        $gateway = PaymentGateway::where('slug', 'paypal')->where('is_active', true)->first();
        if (!$gateway) {
            Log::warning('PayPal webhook received but gateway is not configured/active');
            return response('PayPal gateway is not configured.', 400);
        }

        // Signature verification — uses PayPal's `/v1/notifications/
        // verify-webhook-signature` API via the existing driver helper.
        // Drops the request if the headers are missing OR PayPal says the
        // signature is invalid.
        try {
            $driver = $this->manager->driverFromModel($gateway);
            $ok = $driver->verifyWebhookSignature($rawBody, $headers);
        } catch (\Throwable $e) {
            Log::error('PayPal webhook signature verification crashed', [
                'error' => $e->getMessage(),
            ]);
            return response('Signature verification error.', 400);
        }

        if (!$ok) {
            Log::warning('PayPal webhook signature invalid', [
                'ip' => $request->ip(),
                'has_transmission_id' => !empty($headers['paypal-transmission-id']),
            ]);
            return response('Invalid signature.', 400);
        }

        $payload = json_decode($rawBody, true);
        if (!is_array($payload)) {
            return response('Malformed body.', 400);
        }

        $eventId   = (string) ($payload['id'] ?? '');
        $eventType = (string) ($payload['event_type'] ?? '');
        $resource  = (array)  ($payload['resource']   ?? []);

        if ($eventId === '' || $eventType === '') {
            return response('Missing event id or type.', 400);
        }

        // 24h idempotency — PayPal retries on non-200 for 25 hours.
        $cacheKey = "paypal_webhook:{$eventId}";
        if (Cache::has($cacheKey)) {
            Log::debug('PayPal webhook: duplicate event skipped', ['event_id' => $eventId]);
            return response('Already processed.', 200);
        }

        try {
            $this->dispatchEvent($eventType, $resource, $payload);
            Cache::put($cacheKey, true, now()->addHours(24));
        } catch (\Throwable $e) {
            // Log + 200 so PayPal doesn't hammer us. The admin can replay
            // the event manually from the PayPal Dashboard if needed.
            Log::error('PayPal webhook handler failed', [
                'event_id'   => $eventId,
                'event_type' => $eventType,
                'error'      => $e->getMessage(),
                'trace'      => $e->getTraceAsString(),
            ]);
        }

        return response('OK.', 200);
    }

    /**
     * Route the event to the per-type handler. Unknown events are logged
     * and ignored so the seller can subscribe to anything they want in
     * the PayPal Dashboard without crashing us.
     */
    private function dispatchEvent(string $type, array $resource, array $envelope): void
    {
        match ($type) {
            'PAYMENT.CAPTURE.COMPLETED' => $this->handleCaptureCompleted($resource),
            'PAYMENT.CAPTURE.REFUNDED',
            'PAYMENT.CAPTURE.REVERSED'  => $this->handleRefunded($resource, $envelope),
            'PAYMENT.CAPTURE.DENIED'    => $this->handleCaptureDenied($resource),
            'BILLING.SUBSCRIPTION.ACTIVATED' => $this->handleSubscriptionActivated($resource),
            'BILLING.SUBSCRIPTION.CANCELLED',
            'BILLING.SUBSCRIPTION.SUSPENDED'  => $this->handleSubscriptionEnded($resource),
            'BILLING.SUBSCRIPTION.PAYMENT.FAILED' => $this->handleSubscriptionPaymentFailed($resource),
            default => Log::info("Unhandled PayPal webhook event: {$type}"),
        };
    }

    /**
     * Capture completed — promote a pending Payment row to succeeded.
     * Match by either the order id (PayPal's id stored on the resource's
     * supplementary_data) or by our internal `payment_id` echoed in the
     * `custom_id` field we set when initiating.
     */
    private function handleCaptureCompleted(array $resource): void
    {
        $payment = $this->findPayment($resource);
        if (!$payment) return;

        if ($payment->status === 'succeeded') return; // already reconciled
        $payment->forceFill([
            'status'                 => 'succeeded',
            'gateway_transaction_id' => (string) ($resource['id'] ?? $payment->gateway_transaction_id),
        ])->save();
    }

    /**
     * Refund (full or partial) OR reversal — both paths converge here.
     * PayPal's refund resource carries `amount.value` (decimal string)
     * which is the THIS-refund amount; we accumulate against our own
     * `refund_amount` column so partial refunds reconcile correctly
     * across multiple webhook deliveries.
     */
    private function handleRefunded(array $resource, array $envelope): void
    {
        $payment = $this->findPayment($resource, $envelope);
        if (!$payment) return;

        $thisRefund = (float) ($resource['amount']['value'] ?? 0);
        $currency   = strtoupper((string) ($resource['amount']['currency_code'] ?? $payment->currency));
        $cumulative = (float) ($payment->refund_amount ?? 0) + $thisRefund;
        $original   = (float) $payment->amount;

        $status = $cumulative >= $original ? 'refunded' : 'partially_refunded';

        $payment->forceFill([
            'status'        => $status,
            'refund_amount' => $cumulative,
            'refunded_at'   => now(),
        ])->save();

        Log::info('PayPal refund webhook reconciled', [
            'payment_id'    => $payment->id,
            'status'        => $status,
            'this_refund'   => $thisRefund,
            'cumulative'    => $cumulative,
            'original'      => $original,
            'currency'      => $currency,
        ]);
    }

    private function handleCaptureDenied(array $resource): void
    {
        $payment = $this->findPayment($resource);
        if (!$payment) return;

        $payment->forceFill([
            'status'         => 'failed',
            'failure_reason' => (string) ($resource['status_details']['reason'] ?? 'Capture denied by PayPal.'),
        ])->save();
    }

    /**
     * PayPal subscription went live — mirror to local Subscription if we
     * stamped the workspace + plan in `custom_id` at create time.
     */
    private function handleSubscriptionActivated(array $resource): void
    {
        $custom = (string) ($resource['custom_id'] ?? '');
        [$workspaceId, $planId, $cycle] = $this->parseCustomId($custom);
        if (!$workspaceId || !$planId) return;

        $now = now();
        $ends = $cycle === 'yearly' ? $now->copy()->addYear() : $now->copy()->addMonth();

        Subscription::updateOrCreate(
            [
                'workspace_id'         => $workspaceId,
                'plan_id'              => $planId,
                'stripe_subscription_id' => null,
            ],
            [
                'status'               => 'active',
                'billing_cycle'        => $cycle ?: 'monthly',
                'current_period_start' => $now,
                'current_period_end'   => $ends,
            ],
        );
    }

    private function handleSubscriptionEnded(array $resource): void
    {
        $custom = (string) ($resource['custom_id'] ?? '');
        [$workspaceId, $planId] = $this->parseCustomId($custom);
        if (!$workspaceId || !$planId) return;

        Subscription::where('workspace_id', $workspaceId)
            ->where('plan_id', $planId)
            ->where('status', 'active')
            ->update([
                'status'      => 'canceled',
                'canceled_at' => now(),
            ]);
    }

    private function handleSubscriptionPaymentFailed(array $resource): void
    {
        $custom = (string) ($resource['custom_id'] ?? '');
        [$workspaceId, $planId] = $this->parseCustomId($custom);
        if (!$workspaceId || !$planId) return;

        Subscription::where('workspace_id', $workspaceId)
            ->where('plan_id', $planId)
            ->where('status', 'active')
            ->update(['status' => 'past_due']);
    }

    // ----------------------------------------------------------------

    /**
     * Resolve a Payment row given a PayPal webhook resource.
     *
     * Order of lookup (most specific → least):
     *   1. `resource.custom_id`        — our payment_id we set at initiate
     *   2. `envelope.resource.id`      — PayPal capture/refund id
     *   3. `links → up` href           — order id (extracted from URL)
     *   4. `resource.invoice_id`       — sometimes echoes our payment_id
     */
    private function findPayment(array $resource, array $envelope = []): ?Payment
    {
        // 1. custom_id (preferred — we set this to our payment_id)
        $custom = (string) ($resource['custom_id'] ?? $resource['invoice_id'] ?? '');
        if ($custom !== '' && ctype_digit($custom)) {
            $p = Payment::find((int) $custom);
            if ($p) return $p;
        }

        // 2. PayPal capture id stored on gateway_transaction_id
        $captureId = (string) ($resource['id'] ?? '');
        if ($captureId !== '') {
            $p = Payment::where('gateway_transaction_id', $captureId)->first();
            if ($p) return $p;
        }

        // 3. Order id — extract from the `up` link if present
        foreach (($resource['links'] ?? []) as $link) {
            if (($link['rel'] ?? '') === 'up' && !empty($link['href'])) {
                $orderId = basename($link['href']);
                $p = Payment::where('gateway_transaction_id', $orderId)->first();
                if ($p) return $p;
            }
        }

        Log::warning('PayPal webhook: no matching Payment row', [
            'resource_id'   => $resource['id'] ?? null,
            'custom_id'     => $resource['custom_id'] ?? null,
            'invoice_id'    => $resource['invoice_id'] ?? null,
        ]);
        return null;
    }

    /**
     * `custom_id` is set when WE create the PayPal order. Format:
     *   "{workspace_id}:{plan_id}:{billing_cycle}"
     * Returns [workspace_id, plan_id, billing_cycle] or all nulls.
     */
    private function parseCustomId(string $custom): array
    {
        if ($custom === '') return [null, null, null];
        $parts = explode(':', $custom);
        return [
            isset($parts[0]) && ctype_digit($parts[0]) ? (int) $parts[0] : null,
            isset($parts[1]) && ctype_digit($parts[1]) ? (int) $parts[1] : null,
            $parts[2] ?? null,
        ];
    }

    /**
     * Lowercase + flatten the multi-value header array Laravel exposes
     * so the driver's case-insensitive PayPal-Transmission-Id lookups
     * work regardless of casing the proxy chose.
     */
    private function normalizeHeaders(array $headers): array
    {
        $out = [];
        foreach ($headers as $name => $values) {
            $out[strtolower($name)] = is_array($values) ? ($values[0] ?? '') : (string) $values;
        }
        return $out;
    }
}
