<?php

namespace App\Http\Livewire\Posts;

use App\Http\Livewire\Traits\RequiresAdminAuthorization;
use App\Models\Category;
use App\Models\Locations\CityLocale;
use App\Models\Locations\CountryLocale;
use App\Models\Locations\DistrictLocale;
use App\Models\Locations\DivisionLocale;
use App\Models\Locations\Location;
use App\Models\Meeting;
use App\Models\Post;
use App\Models\PostTranslation;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Laravel\Scout\Jobs\MakeSearchable;
use Livewire\Component;
use WireUi\Traits\WireUiActions;
use ZipArchive;

class BackupRestore extends Component
{
    use WireUiActions;
    use RequiresAdminAuthorization;

    public bool $showRestoreModal = false;

    // Chunked upload state
    public ?string $uploadId = null;
    public ?string $uploadedFilePath = null;
    public ?string $selectedFileName = null;

    // Optional: show "Backup selected" button (requires parent component to provide selection)
    public bool $showBackupSelected = false;
    public array $selectedTranslationIds = [];

    // Restore state
    public array $restorePreview = [];
    public array $restorePostList = [];       // Lightweight summaries for selection display
    public array $selectedPostIndices = [];   // Selected post indices from backup
    public bool $selectAllPosts = true;
    public bool $isRestoring = false;
    public string $duplicateAction = 'skip'; // skip, overwrite
    public array $restoreStats = [];

    protected $listeners = [
        'refreshComponent' => '$refresh',
        'updateSelectedTranslationIds' => 'updateSelectedTranslationIds',
    ];

    public function mount(bool $showBackupSelected = false)
    {
        // Verify admin access
        $this->authorizeAdminAccess();
        $this->showBackupSelected = $showBackupSelected;
    }

    /**
     * Update selected translation IDs from parent component.
     */
    public function updateSelectedTranslationIds(array $ids)
    {
        $this->selectedTranslationIds = $ids;
    }

    /**
     * Toggle select/deselect all posts for restore.
     */
    public function toggleSelectAll()
    {
        if ($this->selectAllPosts) {
            $this->selectedPostIndices = array_column($this->restorePostList, 'index');
        } else {
            $this->selectedPostIndices = [];
        }
    }

    /**
     * Update selectAllPosts state when individual checkboxes change.
     */
    public function updatedSelectedPostIndices()
    {
        $this->selectAllPosts = count($this->selectedPostIndices) === count($this->restorePostList);
    }

    /**
     * Generate and download backup for all posts.
     */
    public function backup()
    {
        $this->authorizeAdminAccess();

        // Pass query builder (not ->get()) so generateBackup can chunk it
        $posts = Post::query();

        return $this->generateBackup($posts, 'posts_backup_');
    }

    /**
     * Generate and download backup for selected posts only.
     */
    public function backupSelected()
    {
        $this->authorizeAdminAccess();

        if (empty($this->selectedTranslationIds)) {
            $this->notification()->error(
                __('Error'),
                __('No posts selected')
            );
            return;
        }

        // Get unique post IDs from selected translation IDs
        $postIds = PostTranslation::whereIn('id', $this->selectedTranslationIds)
            ->pluck('post_id')
            ->unique()
            ->toArray();

        // Pass query builder (not ->get()) so generateBackup can chunk it
        $posts = Post::whereIn('id', $postIds);

        return $this->generateBackup($posts, 'posts_selected_backup_');
    }

    /**
     * Generate backup data and return as download response (ZIP archive with media).
     *
     * @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Collection $posts Query builder or collection
     */
    private function generateBackup($posts, string $filenamePrefix)
    {
        // Build category type lookup (id => type)
        $categoryTypes = Category::pluck('type', 'id')->toArray();

        $filename = $filenamePrefix . now()->format('Ymd_His') . '.zip';

        // Create temporary files
        $tempDir = storage_path('app/temp');
        if (!File::isDirectory($tempDir)) {
            File::makeDirectory($tempDir, 0755, true);
        }
        $tempPath = $tempDir . '/' . uniqid('backup_') . '.zip';
        $tempJsonPath = $tempDir . '/' . uniqid('backup_json_') . '.json';

        // Track counts for meta
        $counts = ['posts' => 0, 'post_translations' => 0, 'meetings' => 0, 'media_files' => 0];

        // Track media files to include in ZIP
        $mediaFiles = [];

        // Write posts as JSON incrementally to a temp file to avoid holding everything in memory
        $jsonHandle = fopen($tempJsonPath, 'w');
        // Write a placeholder for meta - will be replaced via a separate meta file in the ZIP
        fwrite($jsonHandle, '{"meta":"__PLACEHOLDER__","posts":[');

        $isFirst = true;
        $chunkSize = 100;

        // Process posts in chunks to limit memory usage
        $processPost = function ($post) use ($categoryTypes, $jsonHandle, &$isFirst, &$counts, &$mediaFiles) {
            $categoryType = $categoryTypes[$post->category_id] ?? null;

            $postData = [
                'category_type' => $categoryType,
                'love_reactant_id' => $post->love_reactant_id,
                'author_id' => $post->author_id,
                'author_model' => $post->author_model,
                'created_at' => $this->formatDate($post->created_at),
                'updated_at' => $this->formatDate($post->updated_at),
                'translations' => [],
                'meeting' => null,
                'media' => null,
            ];

            foreach ($post->translations as $translation) {
                $postData['translations'][] = [
                    'locale' => $translation->locale,
                    'slug' => $translation->slug,
                    'title' => $translation->title,
                    'excerpt' => $translation->excerpt,
                    'content' => $translation->content,
                    'status' => $translation->status,
                    'updated_by_user_id' => $translation->updated_by_user_id,
                    'from' => $this->formatDate($translation->from),
                    'till' => $this->formatDate($translation->till),
                    'created_at' => $this->formatDate($translation->created_at),
                    'updated_at' => $this->formatDate($translation->updated_at),
                ];
                $counts['post_translations']++;
            }

            if ($post->meeting) {
                $meeting = $post->meeting;
                $postData['meeting'] = [
                    'meetingable_type' => $meeting->meetingable_type,
                    'meetingable_name' => $meeting->meetingable?->name,
                    'venue' => $meeting->venue,
                    'address' => $meeting->address,
                    'price' => $meeting->price,
                    'based_on_quantity' => $meeting->based_on_quantity,
                    'transaction_type_id' => $meeting->transaction_type_id,
                    'status' => $meeting->status,
                    'from' => $this->formatDate($meeting->from),
                    'till' => $this->formatDate($meeting->till),
                    'created_at' => $this->formatDate($meeting->created_at),
                    'updated_at' => $this->formatDate($meeting->updated_at),
                    'location' => $this->getLocationNames($meeting->location),
                ];
                $counts['meetings']++;
            }

            $media = $post->getFirstMedia('posts');
            if ($media) {
                $originalPath = $media->getPath();
                if (File::exists($originalPath)) {
                    $archivePath = "media/{$post->id}/{$media->file_name}";
                    $postData['media'] = [
                        'name' => $media->name,
                        'file_name' => $media->file_name,
                        'mime_type' => $media->mime_type,
                        'size' => $media->size,
                        'collection_name' => $media->collection_name,
                        'custom_properties' => $media->custom_properties,
                        'archive_path' => $archivePath,
                    ];
                    $mediaFiles[] = [
                        'source' => $originalPath,
                        'archive_path' => $archivePath,
                    ];
                    $counts['media_files']++;
                }
            }

            if (!$isFirst) {
                fwrite($jsonHandle, ',');
            }
            fwrite($jsonHandle, json_encode($postData, JSON_UNESCAPED_UNICODE));
            $isFirst = false;
            $counts['posts']++;
        };

        // Use chunking for query builders, iterate for collections
        if ($posts instanceof \Illuminate\Database\Eloquent\Builder) {
            $posts->with(['translations', 'meeting.location', 'media'])
                ->chunk($chunkSize, function ($chunk) use ($processPost) {
                    foreach ($chunk as $post) {
                        $processPost($post);
                    }
                });
        } else {
            foreach ($posts as $post) {
                $processPost($post);
            }
        }

        // Close JSON array
        fwrite($jsonHandle, ']}');
        fclose($jsonHandle);

        // Build meta JSON
        $meta = json_encode([
            'version' => '2.0',
            'created_at' => now()->toIso8601String(),
            'source_database' => config('database.connections.mysql.database'),
            'includes_media' => true,
            'counts' => $counts,
        ], JSON_UNESCAPED_UNICODE);

        // Replace the placeholder in the temp JSON file without reading it all into memory
        // Use a second temp file and stream-copy with the replacement
        $finalJsonPath = $tempJsonPath . '.final';
        $inHandle = fopen($tempJsonPath, 'r');
        $outHandle = fopen($finalJsonPath, 'w');

        // Read the placeholder prefix, replace it, then stream the rest
        $prefix = fread($inHandle, strlen('{"meta":"__PLACEHOLDER__"'));
        fwrite($outHandle, '{"meta":' . $meta);

        // Stream the rest of the file in small chunks
        while (!feof($inHandle)) {
            fwrite($outHandle, fread($inHandle, 8192));
        }

        fclose($inHandle);
        fclose($outHandle);
        @unlink($tempJsonPath);
        rename($finalJsonPath, $tempJsonPath);

        // Create ZIP archive
        $zip = new ZipArchive();
        if ($zip->open($tempPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
            @unlink($tempJsonPath);
            $this->notification()->error(
                __('Error'),
                __('Failed to create ZIP archive')
            );
            return;
        }

        $zip->addFile($tempJsonPath, 'backup.json');

        foreach ($mediaFiles as $mediaFile) {
            if (File::exists($mediaFile['source'])) {
                $zip->addFile($mediaFile['source'], $mediaFile['archive_path']);
            }
        }

        $zip->close();
        @unlink($tempJsonPath);

        // Move ZIP to storage for download via dedicated route (bypasses Livewire response buffering)
        $backupsDir = storage_path('app/backups');
        if (!File::isDirectory($backupsDir)) {
            File::makeDirectory($backupsDir, 0755, true);
        }
        File::move($tempPath, $backupsDir . '/' . $filename);

        $this->notification()->success(
            __('Backup created'),
            __('Downloaded :count posts with :media media files', [
                'count' => $counts['posts'],
                'media' => $counts['media_files'],
            ])
        );

        // Dispatch browser event to trigger download via dedicated HTTP route
        $this->dispatch('backup-ready', filename: $filename);
    }

    /**
     * Open the restore modal.
     */
    public function openRestoreModal()
    {
        $this->authorizeAdminAccess();
        $this->cleanupTempFile();
        $this->reset(['restorePreview', 'restoreStats', 'duplicateAction', 'restorePostList', 'selectedPostIndices', 'selectAllPosts', 'uploadId', 'uploadedFilePath', 'selectedFileName']);
        $this->showRestoreModal = true;
    }

    /**
     * Receive lightweight preview data parsed client-side by JavaScript.
     * JS extracts meta + post summaries from the backup JSON so we don't
     * need to send the entire multi-MB JSON string over the wire.
     *
     * @param array $meta  The backup meta object
     * @param array $postSummaries  Array of {index, title, slug, locales, slugs, has_meeting, has_media, category_type}
     * @param string $fileName  Original file name
     * @param bool $isZip  Whether the file is a ZIP archive
     */
    public function parseBackupPreview(array $meta, array $postSummaries, string $fileName, bool $isZip)
    {
        $this->selectedFileName = $fileName;

        try {
            // Collect all slugs for duplicate checking
            $allSlugs = [];
            foreach ($postSummaries as $summary) {
                foreach ($summary['slugs'] ?? [] as $slug) {
                    $allSlugs[] = $slug;
                }
            }

            $existingSlugs = PostTranslation::withTrashed()
                ->whereIn('slug', $allSlugs)
                ->pluck('slug')
                ->toArray();

            $this->restorePreview = [
                'version' => $meta['version'] ?? 'unknown',
                'created_at' => $meta['created_at'] ?? 'unknown',
                'source_database' => $meta['source_database'] ?? 'unknown',
                'posts' => $meta['counts']['posts'] ?? count($postSummaries),
                'translations' => $meta['counts']['post_translations'] ?? 0,
                'meetings' => $meta['counts']['meetings'] ?? 0,
                'media_files' => $meta['counts']['media_files'] ?? 0,
                'includes_media' => $meta['includes_media'] ?? false,
                'duplicates' => count($existingSlugs),
                'duplicate_slugs' => array_slice($existingSlugs, 0, 10),
                'is_zip' => $isZip,
            ];

            // Store the post summaries for selection UI (already lightweight)
            $this->restorePostList = $postSummaries;
            $this->selectedPostIndices = array_column($postSummaries, 'index');
            $this->selectAllPosts = true;

        } catch (\Exception $e) {
            $this->addError('restoreFile', __('Error reading file: ') . $e->getMessage());
        }
    }

    /**
     * Set the uploaded file path from a completed chunked upload.
     * Called by JS after chunk upload finalization succeeds.
     */
    public function setUploadedFilePath(string $uploadId)
    {
        $this->authorizeAdminAccess();

        $sessionKey = "backup_restore_file_{$uploadId}";
        $path = session($sessionKey);

        if (!$path || !File::exists($path)) {
            $this->notification()->error(__('Error'), __('Uploaded file not found'));
            return;
        }

        $this->uploadId = $uploadId;
        $this->uploadedFilePath = $path;

        // Clean up session key
        session()->forget($sessionKey);
    }

    /**
     * Execute the restore operation.
     */
    public function restore()
    {
        $this->authorizeAdminAccess();

        if (!$this->uploadedFilePath || !File::exists($this->uploadedFilePath)) {
            $this->notification()->error(__('Error'), __('No file uploaded. Please upload the file first.'));
            return;
        }

        $this->isRestoring = true;
        $extractDir = null;

        try {
            $filePath = $this->uploadedFilePath;
            $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
            $isZip = $extension === 'zip';

            $data = null;

            if ($isZip) {
                // Extract ZIP archive
                $extractDir = storage_path('app/temp/restore_' . uniqid());
                File::makeDirectory($extractDir, 0755, true);

                $zip = new ZipArchive();
                if ($zip->open($filePath) !== true) {
                    $this->notification()->error(__('Error'), __('Failed to open ZIP archive'));
                    $this->isRestoring = false;
                    return;
                }

                // Validate ZIP entries to prevent zip-slip path traversal attacks
                for ($i = 0; $i < $zip->numFiles; $i++) {
                    $entryName = $zip->getNameIndex($i);
                    $fullPath = realpath($extractDir) . '/' . $entryName;
                    if (strpos($fullPath, realpath($extractDir)) !== 0) {
                        $zip->close();
                        File::deleteDirectory($extractDir);
                        $this->notification()->error(__('Error'), __('ZIP archive contains unsafe file paths'));
                        $this->isRestoring = false;
                        return;
                    }
                }

                $zip->extractTo($extractDir);
                $zip->close();

                $jsonPath = "{$extractDir}/backup.json";
                if (!File::exists($jsonPath)) {
                    File::deleteDirectory($extractDir);
                    $this->notification()->error(__('Error'), __('Invalid ZIP archive: missing backup.json'));
                    $this->isRestoring = false;
                    return;
                }

                $data = json_decode(File::get($jsonPath), true);
            } else {
                $data = json_decode(file_get_contents($filePath), true);
            }

            // Get active profile from session
            $profileId = session('activeProfileId');
            $profileType = session('activeProfileType');

            if (!$profileId || !$profileType) {
                if ($extractDir) {
                    File::deleteDirectory($extractDir);
                }
                $this->notification()->error(__('Error'), __('No active profile in session'));
                $this->isRestoring = false;
                return;
            }

            // Build category type => id lookup
            $categoryLookup = Category::pluck('id', 'type')->toArray();

            $stats = [
                'posts_created' => 0,
                'posts_skipped' => 0,
                'posts_overwritten' => 0,
                'translations_created' => 0,
                'meetings_created' => 0,
                'media_restored' => 0,
                'media_skipped' => 0,
            ];

            $createdPostIds = [];

            DB::beginTransaction();

            // Filter to only selected posts
            $selectedIndices = array_flip($this->selectedPostIndices);

            // Disable Scout indexing during bulk restore to prevent timeout
            Post::withoutSyncingToSearch(function () use ($data, $profileId, $profileType, $categoryLookup, &$stats, &$createdPostIds, $extractDir, $isZip, $selectedIndices) {

            foreach ($data['posts'] as $index => $postData) {
                if (!isset($selectedIndices[$index])) {
                    continue;
                }
                // Look up category_id by category_type
                $categoryId = null;
                if (!empty($postData['category_type'])) {
                    $categoryId = $categoryLookup[$postData['category_type']] ?? null;
                }

                // Check for existing slugs
                if (!empty($postData['translations'])) {
                    $existingSlugs = PostTranslation::withTrashed()
                        ->whereIn('slug', array_column($postData['translations'], 'slug'))
                        ->pluck('slug')
                        ->toArray();

                    if (!empty($existingSlugs)) {
                        if ($this->duplicateAction === 'skip') {
                            $stats['posts_skipped']++;
                            continue;
                        } elseif ($this->duplicateAction === 'overwrite') {
                            // Delete existing translations and their posts
                            $existingTranslations = PostTranslation::withTrashed()
                                ->whereIn('slug', $existingSlugs)
                                ->get();

                            foreach ($existingTranslations as $existingTranslation) {
                                $postId = $existingTranslation->post_id;
                                $existingTranslation->forceDelete();

                                $remainingTranslations = PostTranslation::withTrashed()
                                    ->where('post_id', $postId)
                                    ->count();

                                if ($remainingTranslations === 0) {
                                    $existingPost = Post::withTrashed()->find($postId);
                                    if ($existingPost) {
                                        // Clear media before deleting post
                                        $existingPost->clearMediaCollection('posts');
                                        Meeting::withTrashed()->where('post_id', $postId)->forceDelete();
                                        $existingPost->forceDelete();
                                    }
                                }
                            }
                            $stats['posts_overwritten']++;
                        }
                    }
                }

                // Create post
                $post = new Post();
                $post->postable_id = $profileId;
                $post->postable_type = $profileType;
                $post->category_id = $categoryId;
                // Don't set love_reactant_id - let PostObserver register it as reactant
                $post->author_id = null; // Author IDs are not portable between databases
                $post->author_model = null;
                $post->created_at = $postData['created_at'] ? new \DateTime($postData['created_at']) : now();
                $post->updated_at = $postData['updated_at'] ? new \DateTime($postData['updated_at']) : now();
                $post->save();

                // Ensure post is registered as reactant (in case observer didn't fire)
                if (!$post->isRegisteredAsLoveReactant()) {
                    $post->registerAsLoveReactant();
                }

                // Create translations
                foreach ($postData['translations'] as $translationData) {
                    $translation = new PostTranslation();
                    $translation->post_id = $post->id;
                    $translation->locale = $translationData['locale'];
                    $translation->slug = $translationData['slug'];
                    $translation->title = $translationData['title'];
                    $translation->excerpt = $translationData['excerpt'];
                    $translation->content = $translationData['content'];
                    $translation->status = $translationData['status'];
                    $translation->updated_by_user_id = $translationData['updated_by_user_id'];
                    $translation->from = $translationData['from'] ? new \DateTime($translationData['from']) : null;
                    $translation->till = $translationData['till'] ? new \DateTime($translationData['till']) : null;
                    $translation->created_at = $translationData['created_at'] ? new \DateTime($translationData['created_at']) : now();
                    $translation->updated_at = $translationData['updated_at'] ? new \DateTime($translationData['updated_at']) : now();
                    $translation->save();

                    $stats['translations_created']++;
                }

                // Create meeting (hasOne relationship)
                if (!empty($postData['meeting'])) {
                    $meetingData = $postData['meeting'];

                    // Look up meetingable by name and type
                    $meetingableId = null;
                    $meetingableType = null;
                    // Whitelist of allowed meetingable types to prevent arbitrary class instantiation
                    $allowedMeetingableTypes = [
                        \App\Models\User::class,
                        \App\Models\Organization::class,
                        \App\Models\Bank::class,
                    ];
                    if (!empty($meetingData['meetingable_type']) && !empty($meetingData['meetingable_name'])) {
                        $meetingableType = $meetingData['meetingable_type'];
                        if (in_array($meetingableType, $allowedMeetingableTypes, true)) {
                            $meetingable = $meetingableType::where('name', $meetingData['meetingable_name'])->first();
                            if ($meetingable) {
                                $meetingableId = $meetingable->id;
                            }
                        }
                    }

                    $meeting = new Meeting();
                    $meeting->post_id = $post->id;
                    $meeting->meetingable_id = $meetingableId;
                    $meeting->meetingable_type = $meetingableId ? $meetingableType : null;
                    $meeting->venue = $meetingData['venue'];
                    $meeting->address = $meetingData['address'];
                    $meeting->price = $meetingData['price'];
                    $meeting->based_on_quantity = $meetingData['based_on_quantity'];
                    $meeting->transaction_type_id = $meetingData['transaction_type_id'];
                    $meeting->status = $meetingData['status'];
                    $meeting->from = $meetingData['from'] ? new \DateTime($meetingData['from']) : null;
                    $meeting->till = $meetingData['till'] ? new \DateTime($meetingData['till']) : null;
                    $meeting->created_at = $meetingData['created_at'] ? new \DateTime($meetingData['created_at']) : now();
                    $meeting->updated_at = $meetingData['updated_at'] ? new \DateTime($meetingData['updated_at']) : now();
                    $meeting->save();

                    // Create location if location data exists
                    if (!empty($meetingData['location'])) {
                        $locationIds = $this->lookupLocationIds($meetingData['location']);
                        if ($locationIds['country_id'] || $locationIds['division_id'] || $locationIds['city_id'] || $locationIds['district_id']) {
                            $location = new Location();
                            $location->locatable_id = $meeting->id;
                            $location->locatable_type = Meeting::class;
                            $location->country_id = $locationIds['country_id'];
                            $location->division_id = $locationIds['division_id'];
                            $location->city_id = $locationIds['city_id'];
                            $location->district_id = $locationIds['district_id'];
                            $location->save();
                        }
                    }

                    $stats['meetings_created']++;
                }

                // Restore media if available
                if ($isZip && $extractDir && !empty($postData['media'])) {
                    $mediaData = $postData['media'];
                    $mediaPath = "{$extractDir}/{$mediaData['archive_path']}";

                    // Prevent path traversal via crafted archive_path in JSON
                    $realMediaPath = realpath($mediaPath);
                    $realExtractDir = realpath($extractDir);
                    if ($realMediaPath && $realExtractDir && strpos($realMediaPath, $realExtractDir) === 0 && File::exists($mediaPath)) {
                        try {
                            $media = $post->addMedia($mediaPath)
                                ->preservingOriginal() // Don't delete from extract dir yet
                                ->usingName($mediaData['name'])
                                ->usingFileName($mediaData['file_name'])
                                ->withCustomProperties($mediaData['custom_properties'] ?? [])
                                ->toMediaCollection('posts');

                            // Dispatch conversion job to queue
                            $conversionCollection = \Spatie\MediaLibrary\Conversions\ConversionCollection::createForMedia($media);
                            if ($conversionCollection->isNotEmpty()) {
                                dispatch(new \Spatie\MediaLibrary\Conversions\Jobs\PerformConversionsJob($conversionCollection, $media, false))
                                    ->onQueue('low');
                            }

                            $stats['media_restored']++;
                        } catch (\Exception $e) {
                            $stats['media_skipped']++;
                        }
                    } else {
                        $stats['media_skipped']++;
                    }
                }

                $createdPostIds[] = $post->id;
                $stats['posts_created']++;
            }

            }); // End withoutSyncingToSearch

            DB::commit();

            // Clean up extracted files
            if ($extractDir) {
                File::deleteDirectory($extractDir);
            }

            // Queue posts for search indexing on 'low' queue in chunks
            if (!empty($createdPostIds)) {
                foreach (array_chunk($createdPostIds, 50) as $chunk) {
                    $posts = Post::whereIn('id', $chunk)->get();
                    dispatch(new MakeSearchable($posts))->onQueue('low');
                }
            }

            $this->restoreStats = $stats;

            // Clean up uploaded temp file and free memory
            if ($this->uploadedFilePath && File::exists($this->uploadedFilePath)) {
                @unlink($this->uploadedFilePath);
            }
            $this->uploadedFilePath = null;
            $this->uploadId = null;
            $this->restorePreview = [];
            $this->restorePostList = [];
            $this->selectedPostIndices = [];

            $mediaMsg = $stats['media_restored'] > 0 ? " with {$stats['media_restored']} media files" : '';
            $this->notification()->success(
                __('Restore completed'),
                __('Created :count posts', ['count' => $stats['posts_created']]) . $mediaMsg
            );

            // Refresh parent posts table
            $this->dispatch('refreshPostsTable')->to('posts.manage');

        } catch (\Exception $e) {
            DB::rollBack();
            if ($extractDir && File::isDirectory($extractDir)) {
                File::deleteDirectory($extractDir);
            }
            $this->notification()->error(__('Error'), $e->getMessage());
        }

        $this->isRestoring = false;
    }

    /**
     * Close modal and reset state.
     */
    public function closeRestoreModal()
    {
        $this->showRestoreModal = false;
        $this->cleanupTempFile();
        $this->reset(['restorePreview', 'restoreStats', 'duplicateAction', 'restorePostList', 'selectedPostIndices', 'selectAllPosts', 'uploadId', 'uploadedFilePath', 'selectedFileName']);
    }

    /**
     * Clean up any uploaded temp file.
     */
    private function cleanupTempFile(): void
    {
        if ($this->uploadedFilePath && File::exists($this->uploadedFilePath)) {
            @unlink($this->uploadedFilePath);
        }
    }

    /**
     * Safely format a date value to ISO8601 string.
     */
    private function formatDate($value): ?string
    {
        if ($value === null) {
            return null;
        }

        if ($value instanceof \Carbon\Carbon || $value instanceof \DateTime) {
            return $value->format('c');
        }

        if (is_string($value)) {
            return $value;
        }

        return null;
    }

    /**
     * Get location names in the app's base locale for backup.
     * Names are used for lookup on restore instead of IDs.
     */
    private function getLocationNames($location): ?array
    {
        if (!$location) {
            return null;
        }

        $baseLocale = config('app.locale');

        // Get country name
        $countryName = null;
        if ($location->country_id) {
            $countryLocale = CountryLocale::withoutGlobalScopes()
                ->where('country_id', $location->country_id)
                ->where('locale', $baseLocale)
                ->first();
            $countryName = $countryLocale?->name;
        }

        // Get division name
        $divisionName = null;
        if ($location->division_id) {
            $divisionLocale = DivisionLocale::withoutGlobalScopes()
                ->where('division_id', $location->division_id)
                ->where('locale', $baseLocale)
                ->first();
            $divisionName = $divisionLocale?->name;
        }

        // Get city name
        $cityName = null;
        if ($location->city_id) {
            $cityLocale = CityLocale::withoutGlobalScopes()
                ->where('city_id', $location->city_id)
                ->where('locale', $baseLocale)
                ->first();
            $cityName = $cityLocale?->name;
        }

        // Get district name
        $districtName = null;
        if ($location->district_id) {
            $districtLocale = DistrictLocale::withoutGlobalScopes()
                ->where('district_id', $location->district_id)
                ->where('locale', $baseLocale)
                ->first();
            $districtName = $districtLocale?->name;
        }

        return [
            'country_name' => $countryName,
            'division_name' => $divisionName,
            'city_name' => $cityName,
            'district_name' => $districtName,
        ];
    }

    /**
     * Look up location IDs by names in the app's base locale.
     * Returns null for any location component that cannot be found.
     */
    private function lookupLocationIds(array $locationData): array
    {
        $baseLocale = config('app.locale');

        $result = [
            'country_id' => null,
            'division_id' => null,
            'city_id' => null,
            'district_id' => null,
        ];

        // Look up country by name
        if (!empty($locationData['country_name'])) {
            $countryLocale = CountryLocale::withoutGlobalScopes()
                ->where('name', $locationData['country_name'])
                ->where('locale', $baseLocale)
                ->first();
            $result['country_id'] = $countryLocale?->country_id;
        }

        // Look up division by name
        if (!empty($locationData['division_name'])) {
            $divisionLocale = DivisionLocale::withoutGlobalScopes()
                ->where('name', $locationData['division_name'])
                ->where('locale', $baseLocale)
                ->first();
            $result['division_id'] = $divisionLocale?->division_id;
        }

        // Look up city by name
        if (!empty($locationData['city_name'])) {
            $cityLocale = CityLocale::withoutGlobalScopes()
                ->where('name', $locationData['city_name'])
                ->where('locale', $baseLocale)
                ->first();
            $result['city_id'] = $cityLocale?->city_id;
        }

        // Look up district by name
        if (!empty($locationData['district_name'])) {
            $districtLocale = DistrictLocale::withoutGlobalScopes()
                ->where('name', $locationData['district_name'])
                ->where('locale', $baseLocale)
                ->first();
            $result['district_id'] = $districtLocale?->district_id;
        }

        return $result;
    }

    public function render()
    {
        return view('livewire.posts.backup-restore');
    }
}
