<?php

namespace App\Jobs;

use App\Models\EventPurchase;
use App\Models\IntegrationWebhookEvent;
use App\Models\Tenant;
use App\Services\Integrations\IntegrationWebhookRecorder;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;

class ProcessRevenueCatWebhook implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * @var array<string, mixed>
     */
    private array $payload;

    private ?string $eventId;

    private ?int $webhookEventId;

    public int $tries = 5;

    public int $backoff = 60;

    /**
     * @param  array<string, mixed>  $payload
     */
    public function __construct(array $payload, ?string $eventId = null, ?int $webhookEventId = null)
    {
        $this->payload = $payload;
        $this->eventId = $eventId !== '' ? $eventId : null;
        $this->webhookEventId = $webhookEventId;
        $this->queue = config('services.revenuecat.queue', 'webhooks');
        $this->onQueue($this->queue);
    }

    public function handle(): void
    {
        $webhookEvent = $this->resolveWebhookEvent();
        $appUserId = $this->value('event.app_user_id')
            ?? $this->value('subscriber.app_user_id');

        if (! is_string($appUserId) || $appUserId === '') {
            Log::warning('RevenueCat webhook missing app_user_id', [
                'event_id' => $this->eventId,
            ]);
            $this->markFailed($webhookEvent, 'Missing app_user_id');

            return;
        }

        $tenant = $this->resolveTenant($appUserId);
        if (! $tenant) {
            Log::warning('RevenueCat webhook tenant not found', [
                'event_id' => $this->eventId,
                'app_user_id' => $appUserId,
            ]);
            $this->markFailed($webhookEvent, 'Tenant not found');

            return;
        }

        $productId = $this->value('event.product_id')
            ?? $this->value('event.entitlement_id');
        $credits = $this->mapCreditsFromProduct($productId);

        $transactionId = $this->value('event.transaction_id')
            ?? $this->value('event.id')
            ?? $this->eventId
            ?? (string) Str::uuid();

        if (EventPurchase::where('provider', 'revenuecat')
            ->where('external_receipt_id', $transactionId)
            ->exists()) {
            $this->markIgnored($webhookEvent, 'Duplicate transaction');

            return;
        }

        $amount = (float) ($this->value('event.price') ?? 0);
        $currency = strtoupper((string) ($this->value('event.currency') ?? 'EUR'));

        DB::transaction(function () use ($tenant, $credits, $transactionId, $amount, $currency) {
            $tenant->refresh();

            $purchase = EventPurchase::create([
                'tenant_id' => $tenant->id,
                'events_purchased' => $credits,
                'amount' => $amount,
                'currency' => $currency,
                'provider' => 'revenuecat',
                'external_receipt_id' => $transactionId,
                'status' => 'completed',
                'purchased_at' => now(),
            ]);
        });

        $tenant->refresh();
        $this->updateSubscriptionStatus($tenant);

        Log::info('RevenueCat webhook processed', [
            'event_id' => $this->eventId,
            'tenant_id' => $tenant->id,
            'product_id' => $productId,
            'credits' => $credits,
        ]);

        $this->markProcessed($webhookEvent);
    }

    public function failed(\Throwable $exception): void
    {
        $webhookEvent = $this->resolveWebhookEvent();
        $this->markFailed($webhookEvent, $exception->getMessage());
    }

    private function updateSubscriptionStatus(Tenant $tenant): void
    {
        $expirationMs = $this->value('event.expiration_at_ms');
        $expirationIso = $this->value('event.expiration_at');

        $expiresAt = null;
        if (is_numeric($expirationMs)) {
            $expiresAt = Carbon::createFromTimestampMs((int) $expirationMs, 'UTC');
        } elseif (is_string($expirationIso) && $expirationIso !== '') {
            try {
                $expiresAt = Carbon::parse($expirationIso);
            } catch (\Throwable $e) {
                $expiresAt = null;
            }
        }

        if (! $expiresAt instanceof Carbon) {
            return;
        }

        $expiresAt = $expiresAt->copy()->setTimezone('UTC')->floorSecond();

        $tier = $expiresAt->isFuture() ? 'pro' : 'free';

        $tenant->update([
            'subscription_tier' => $tier,
            'subscription_expires_at' => $expiresAt,
        ]);
    }

    private function resolveTenant(string $appUserId): ?Tenant
    {
        $prefix = (string) config('services.revenuecat.app_user_prefix', 'tenant');
        $lower = strtolower($appUserId);

        if (is_numeric($appUserId)) {
            $byId = Tenant::find((int) $appUserId);
            if ($byId) {
                return $byId;
            }
        }

        foreach ([':', '-', '_'] as $delimiter) {
            $needle = strtolower($prefix).$delimiter;
            if (str_starts_with($lower, $needle)) {
                $candidate = substr($appUserId, strlen($needle));
                if (is_numeric($candidate)) {
                    $byNumeric = Tenant::find((int) $candidate);
                    if ($byNumeric) {
                        return $byNumeric;
                    }
                }

                $bySlug = Tenant::where('slug', $candidate)->first();
                if ($bySlug) {
                    return $bySlug;
                }
            }
        }

        return Tenant::where('slug', $appUserId)->first();
    }

    private function mapCreditsFromProduct(?string $productId): int
    {
        if (! is_string($productId) || $productId === '') {
            return 0;
        }

        $mappings = $this->productMappings();
        if (array_key_exists($productId, $mappings)) {
            return (int) $mappings[$productId];
        }

        return 0;
    }

    /**
     * @return array<string, int>
     */
    private function productMappings(): array
    {
        $raw = (string) config('services.revenuecat.product_mappings', '');
        if ($raw === '') {
            return [];
        }

        $decoded = json_decode($raw, true);
        if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
            $normalized = [];

            foreach ($decoded as $key => $value) {
                if (! is_scalar($value)) {
                    continue;
                }

                $normalized[(string) $key] = (int) $value;
            }

            return $normalized;
        }

        $mappings = [];
        foreach (explode(',', $raw) as $pair) {
            $pair = trim($pair);
            if ($pair === '') {
                continue;
            }

            [$key, $value] = array_pad(explode(':', $pair, 2), 2, null);
            if ($key === null || $value === null) {
                continue;
            }

            $mappings[trim($key)] = (int) trim($value);
        }

        return $mappings;
    }

    private function value(string $path, $default = null): mixed
    {
        $segments = explode('.', $path);
        $value = $this->payload;

        foreach ($segments as $segment) {
            if (! is_array($value) || ! array_key_exists($segment, $value)) {
                return $default;
            }

            $value = $value[$segment];
        }

        return $value;
    }

    private function resolveWebhookEvent(): ?IntegrationWebhookEvent
    {
        if (! $this->webhookEventId) {
            return null;
        }

        return IntegrationWebhookEvent::find($this->webhookEventId);
    }

    private function markProcessed(?IntegrationWebhookEvent $event): void
    {
        if (! $event) {
            return;
        }

        app(IntegrationWebhookRecorder::class)->markProcessed($event);
    }

    private function markIgnored(?IntegrationWebhookEvent $event, string $reason): void
    {
        if (! $event) {
            return;
        }

        app(IntegrationWebhookRecorder::class)->markIgnored($event, ['reason' => $reason]);
    }

    private function markFailed(?IntegrationWebhookEvent $event, string $reason): void
    {
        if (! $event) {
            return;
        }

        app(IntegrationWebhookRecorder::class)->markFailed($event, $reason);
    }
}
