<?php

namespace App\Jobs;

use App\Models\DataExport;
use App\Models\Event;
use App\Models\PackagePurchase;
use App\Models\Tenant;
use App\Models\User;
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\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use ZipArchive;

class GenerateDataExport implements ShouldQueue
{
    use Dispatchable;
    use InteractsWithQueue;
    use Queueable;
    use SerializesModels;

    public function __construct(private readonly int $exportId)
    {
        $this->onQueue('default');
    }

    public function handle(): void
    {
        $export = DataExport::with(['user', 'tenant'])->find($this->exportId);

        if (! $export) {
            return;
        }

        if (! $export->user) {
            $export->update([
                'status' => DataExport::STATUS_FAILED,
                'error_message' => 'User no longer exists.',
            ]);

            return;
        }

        $export->update(['status' => DataExport::STATUS_PROCESSING, 'error_message' => null]);

        try {
            $payload = $this->buildPayload($export->user, $export->tenant);
            $zipPath = $this->writeArchive($export, $payload);
            $export->update([
                'status' => DataExport::STATUS_READY,
                'path' => $zipPath,
                'size_bytes' => Storage::disk('local')->size($zipPath),
                'expires_at' => now()->addDays(14),
            ]);
        } catch (\Throwable $exception) {
            $export->update([
                'status' => DataExport::STATUS_FAILED,
                'error_message' => $exception->getMessage(),
            ]);

            report($exception);
        }
    }

    /**
     * @return array<string, mixed>
     */
    protected function buildPayload(User $user, ?Tenant $tenant): array
    {
        $profile = [
            'generated_at' => now()->toIso8601String(),
            'user' => [
                'id' => $user->id,
                'name' => $user->name,
                'email' => $user->email,
                'username' => $user->username,
                'preferred_locale' => $user->preferred_locale,
                'created_at' => optional($user->created_at)->toIso8601String(),
                'email_verified_at' => optional($user->email_verified_at)->toIso8601String(),
            ],
        ];

        if ($tenant) {
            $profile['tenant'] = [
                'id' => $tenant->id,
                'name' => $tenant->name,
                'slug' => $tenant->slug,
                'contact_email' => $tenant->contact_email,
                'contact_phone' => $tenant->contact_phone,
                'subscription_status' => $tenant->subscription_status,
                'subscription_expires_at' => optional($tenant->subscription_expires_at)->toIso8601String(),
                'created_at' => optional($tenant->created_at)->toIso8601String(),
            ];
        }

        $events = $tenant
            ? $this->collectEvents($tenant)
            : [];
        $invoices = $tenant
            ? $this->collectInvoices($tenant)
            : [];

        return [
            'profile' => $profile,
            'events' => $events,
            'invoices' => $invoices,
        ];
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    protected function collectEvents(Tenant $tenant): array
    {
        $events = Event::query()
            ->withCount([
                'photos as photos_total',
                'photos as featured_photos_total' => fn ($query) => $query->where('is_featured', true),
                'joinTokens',
                'members',
            ])
            ->where('tenant_id', $tenant->id)
            ->orderBy('date')
            ->get();

        $likeCounts = DB::table('photo_likes')
            ->join('photos', 'photo_likes.photo_id', '=', 'photos.id')
            ->where('photos.tenant_id', $tenant->id)
            ->groupBy('photos.event_id')
            ->pluck(DB::raw('COUNT(*)'), 'photos.event_id');

        return $events
            ->map(function (Event $event) use ($likeCounts): array {
                $likes = (int) ($likeCounts[$event->id] ?? 0);

                return [
                    'id' => $event->id,
                    'slug' => $event->slug,
                    'status' => $event->status,
                    'name' => $event->name,
                    'location' => $event->location,
                    'date' => optional($event->date)->toIso8601String(),
                    'photos_total' => (int) ($event->photos_total ?? 0),
                    'featured_photos_total' => (int) ($event->featured_photos_total ?? 0),
                    'join_tokens_total' => (int) ($event->join_tokens_count ?? 0),
                    'members_total' => (int) ($event->members_count ?? 0),
                    'likes_total' => $likes,
                    'created_at' => optional($event->created_at)->toIso8601String(),
                    'updated_at' => optional($event->updated_at)->toIso8601String(),
                ];
            })
            ->all();
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    protected function collectInvoices(Tenant $tenant): array
    {
        return PackagePurchase::query()
            ->where('tenant_id', $tenant->id)
            ->with('package')
            ->latest('purchased_at')
            ->get()
            ->map(function (PackagePurchase $purchase): array {
                return [
                    'id' => $purchase->id,
                    'package' => $purchase->package?->getNameForLocale(app()->getLocale())
                        ?? $purchase->package?->name,
                    'price' => $purchase->price !== null ? (float) $purchase->price : null,
                    'currency' => 'EUR',
                    'type' => $purchase->type,
                    'provider' => $purchase->provider,
                    'provider_id' => $purchase->provider_id,
                    'purchased_at' => optional($purchase->purchased_at)->toIso8601String(),
                    'refunded' => (bool) $purchase->refunded,
                ];
            })
            ->all();
    }

    /**
     * @param  array<string, mixed>  $payload
     */
    protected function writeArchive(DataExport $export, array $payload): string
    {
        $directory = 'exports/user-'.$export->user_id;
        Storage::disk('local')->makeDirectory($directory);
        $filename = sprintf('data-export-%s.zip', Str::uuid());
        $path = $directory.'/'.$filename;

        $zip = new ZipArchive;
        $fullPath = Storage::disk('local')->path($path);

        if ($zip->open($fullPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
            throw new \RuntimeException('Unable to create export archive.');
        }

        $zip->addFromString('profile.json', json_encode($payload['profile'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
        $zip->addFromString('events.json', json_encode($payload['events'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
        $zip->addFromString('invoices.json', json_encode($payload['invoices'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));

        $locale = $export->user?->preferred_locale ?? app()->getLocale();
        $readme = implode("\n", [
            __('profile.export.readme.title', [], $locale),
            '---------------------',
            __('profile.export.readme.description', [], $locale),
            __('profile.export.readme.generated', [
                'date' => now()->copy()->locale($locale)->translatedFormat('d. F Y H:i'),
            ], $locale),
            __('profile.export.readme.expiry', [], $locale),
        ]);
        $zip->addFromString('README.txt', $readme);

        $zip->close();

        return $path;
    }
}
