<?php

namespace App\Jobs;

use App\Enums\DataExportScope;
use App\Models\DataExport;
use App\Models\Event;
use App\Models\EventMediaAsset;
use App\Models\PackagePurchase;
use App\Models\Photo;
use App\Models\Tenant;
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', 'event'])->find($this->exportId);

        if (! $export) {
            return;
        }

        if ($export->status !== DataExport::STATUS_PENDING) {
            return;
        }

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

            return;
        }

        if ($export->scope === DataExportScope::TENANT && ! $export->tenant) {
            $export->update([
                'status' => DataExport::STATUS_FAILED,
                'error_message' => 'Tenant no longer exists.',
            ]);

            return;
        }

        if ($export->scope === DataExportScope::EVENT && ! $export->event) {
            $export->update([
                'status' => DataExport::STATUS_FAILED,
                'error_message' => 'Event no longer exists.',
            ]);

            return;
        }

        if (
            $export->event
            && $export->tenant
            && (int) $export->event->tenant_id !== (int) $export->tenant->id
        ) {
            $export->update([
                'status' => DataExport::STATUS_FAILED,
                'error_message' => 'Event does not belong to tenant.',
            ]);

            return;
        }

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

        try {
            $payload = $this->buildPayload($export);
            $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(DataExport $export): array
    {
        $user = $export->user;
        $tenant = $export->tenant;
        $event = $export->event;

        $profile = [
            'generated_at' => now()->toIso8601String(),
            'scope' => $export->scope?->value ?? DataExportScope::USER->value,
            'include_media' => (bool) $export->include_media,
            '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(),
            ];
        }

        $payload = [
            'profile' => $profile,
        ];

        if ($export->scope === DataExportScope::EVENT && $event) {
            $event->loadCount([
                'photos as photos_total',
                'photos as featured_photos_total' => fn ($query) => $query->where('is_featured', true),
                'joinTokens',
                'members',
            ]);

            $payload['event'] = $this->buildEventSummary(
                $event,
                $this->countEventLikes($event)
            );
            $payload['photos'] = $this->collectEventPhotos($event);
        } else {
            $payload['events'] = $tenant
                ? $this->collectEvents($tenant)
                : [];
        }

        $payload['invoices'] = $tenant
            ? $this->collectInvoices($tenant)
            : [];

        return $payload;
    }

    /**
     * @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(fn (Event $event): array => $this->buildEventSummary(
                $event,
                (int) ($likeCounts[$event->id] ?? 0)
            ))
            ->all();
    }

    /**
     * @return array<string, mixed>
     */
    protected function buildEventSummary(Event $event, ?int $likes = null): array
    {
        $likes = $likes ?? $this->countEventLikes($event);

        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 ?? $event->photos()->count()),
            'featured_photos_total' => (int) ($event->featured_photos_total ?? $event->photos()->where('is_featured', true)->count()),
            'join_tokens_total' => (int) ($event->join_tokens_count ?? $event->joinTokens()->count()),
            'members_total' => (int) ($event->members_count ?? $event->members()->count()),
            'likes_total' => $likes,
            'created_at' => optional($event->created_at)->toIso8601String(),
            'updated_at' => optional($event->updated_at)->toIso8601String(),
        ];
    }

    protected function countEventLikes(Event $event): int
    {
        return (int) DB::table('photo_likes')
            ->join('photos', 'photo_likes.photo_id', '=', 'photos.id')
            ->where('photos.event_id', $event->id)
            ->count();
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    protected function collectEventPhotos(Event $event): array
    {
        return Photo::query()
            ->withCount('likes')
            ->where('event_id', $event->id)
            ->orderBy('created_at')
            ->get()
            ->map(fn (Photo $photo): array => [
                'id' => $photo->id,
                'status' => $photo->status ?? null,
                'ingest_source' => $photo->ingest_source,
                'is_featured' => (bool) $photo->is_featured,
                'emotion_id' => $photo->emotion_id,
                'task_id' => $photo->task_id,
                'likes_total' => (int) ($photo->likes_count ?? 0),
                'created_at' => optional($photo->created_at)->toIso8601String(),
                'moderated_at' => optional($photo->moderated_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 = match ($export->scope) {
            DataExportScope::TENANT => 'exports/tenant-'.$export->tenant_id,
            DataExportScope::EVENT => 'exports/event-'.$export->event_id,
            default => '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('export.json', json_encode([
            'id' => $export->id,
            'scope' => $export->scope?->value ?? DataExportScope::USER->value,
            'include_media' => (bool) $export->include_media,
            'tenant_id' => $export->tenant_id,
            'event_id' => $export->event_id,
            'requested_by' => $export->user?->id,
            'generated_at' => now()->toIso8601String(),
        ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));

        if (array_key_exists('profile', $payload)) {
            $zip->addFromString('profile.json', json_encode($payload['profile'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
        }

        if (array_key_exists('events', $payload)) {
            $zip->addFromString('events.json', json_encode($payload['events'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
        }

        if (array_key_exists('event', $payload)) {
            $zip->addFromString('event.json', json_encode($payload['event'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
        }

        if (array_key_exists('photos', $payload)) {
            $zip->addFromString('photos.json', json_encode($payload['photos'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
        }

        if (array_key_exists('invoices', $payload)) {
            $zip->addFromString('invoices.json', json_encode($payload['invoices'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
        }

        if ($export->include_media) {
            $this->appendMediaFiles($zip, $export);
        }

        $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;
    }

    /**
     * @return array<int, Event>
     */
    protected function resolveMediaEvents(DataExport $export): array
    {
        if ($export->scope === DataExportScope::EVENT && $export->event) {
            return [$export->event];
        }

        if ($export->scope === DataExportScope::TENANT && $export->tenant) {
            return Event::query()
                ->where('tenant_id', $export->tenant->id)
                ->orderBy('date')
                ->get()
                ->all();
        }

        return [];
    }

    protected function appendMediaFiles(ZipArchive $zip, DataExport $export): void
    {
        $events = $this->resolveMediaEvents($export);

        foreach ($events as $event) {
            $photos = Photo::query()
                ->with('mediaAsset')
                ->where('event_id', $event->id)
                ->orderBy('created_at')
                ->get();

            foreach ($photos as $photo) {
                $asset = $this->resolveOriginalAsset($photo);
                $sourcePath = $asset?->path ?? $photo->file_path;

                if (! $sourcePath) {
                    continue;
                }

                $filename = $this->buildMediaFilename($event, $photo, $sourcePath);

                if ($asset && $asset->path) {
                    $this->addDiskFileToArchive(
                        $zip,
                        $asset->disk ?? config('filesystems.default'),
                        $asset->path,
                        $filename
                    );
                } else {
                    $this->addDiskFileToArchive(
                        $zip,
                        config('filesystems.default'),
                        $photo->file_path,
                        $filename
                    );
                }
            }
        }
    }

    protected function resolveOriginalAsset(Photo $photo): ?EventMediaAsset
    {
        $asset = $photo->mediaAsset;

        if ($asset && $asset->variant === 'original') {
            return $asset;
        }

        $original = EventMediaAsset::query()
            ->where('photo_id', $photo->id)
            ->where('variant', 'original')
            ->first();

        return $original ?? $asset;
    }

    protected function buildMediaFilename(Event $event, Photo $photo, string $sourcePath): string
    {
        $eventSlug = $event->slug ?: 'event-'.$event->id;
        $timestamp = $photo->created_at?->format('Ymd_His') ?? now()->format('Ymd_His');
        $extension = pathinfo($sourcePath, PATHINFO_EXTENSION) ?: 'jpg';

        return sprintf('media/%s/%s-photo-%d.%s', $eventSlug, $timestamp, $photo->id, $extension);
    }

    protected function addDiskFileToArchive(ZipArchive $zip, ?string $diskName, string $path, string $filename): bool
    {
        $disk = $diskName ? Storage::disk($diskName) : Storage::disk(config('filesystems.default'));

        try {
            if (method_exists($disk, 'path')) {
                $absolute = $disk->path($path);

                if (is_file($absolute)) {
                    return $zip->addFile($absolute, $filename);
                }
            }

            $stream = $disk->readStream($path);

            if ($stream) {
                $contents = stream_get_contents($stream);
                fclose($stream);

                if ($contents !== false) {
                    return $zip->addFromString($filename, $contents);
                }
            }
        } catch (\Throwable $exception) {
            report($exception);
        }

        return false;
    }
}
