<?php

namespace App\Console\Commands;

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\Meeting;
use App\Models\Post;
use App\Models\PostTranslation;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use ZipArchive;

class BackupPosts extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'posts:backup
                            {--output= : Output file path (default: backups/posts/posts_backup_YYYYMMDD_HHMMSS.zip)}
                            {--post-ids= : Comma-separated list of post IDs to backup (e.g., --post-ids=29,30,405,502)}
                            {--exclude-media : Exclude media files from the backup (creates smaller JSON-only backup)}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Backup posts, post_translations, meetings, and media to a ZIP archive for restoration on another database';

    /**
     * The console command help text.
     *
     * @var string
     */
    protected $help = <<<'HELP'
Examples:
  <info>php artisan posts:backup</info>
      Backup all posts with media to the default location (backups/posts/posts_YYYYMMDD_HHMMSS.zip)

  <info>php artisan posts:backup --output=my_backup.zip</info>
      Backup all posts with media to a custom file path

  <info>php artisan posts:backup --post-ids=29,30,405,502</info>
      Backup only specific posts by their IDs

  <info>php artisan posts:backup --exclude-media</info>
      Backup posts without media files (JSON-only, smaller file size)

  <info>php artisan posts:backup --post-ids=29,30 --output=backups/selected_posts.zip</info>
      Backup specific posts to a custom file path
HELP;

    /**
     * Execute the console command.
     */
    public function handle(): int
    {
        $this->info('Starting posts backup...');

        $excludeMedia = $this->option('exclude-media');

        // Determine output path
        $outputPath = $this->option('output');
        $extension = $excludeMedia ? 'json' : 'zip';

        if (!$outputPath) {
            $backupDir = base_path('backups/posts');
            if (!File::isDirectory($backupDir)) {
                File::makeDirectory($backupDir, 0755, true);
            }
            $timestamp = now()->format('Ymd_His');
            $outputPath = "{$backupDir}/posts_backup_{$timestamp}.{$extension}";
        }

        // Ensure directory exists
        $directory = dirname($outputPath);
        if (!File::isDirectory($directory)) {
            File::makeDirectory($directory, 0755, true);
        }

        // Build posts query (only non-deleted posts)
        $query = Post::query();

        // Filter by specific post IDs if provided
        $postIdsOption = $this->option('post-ids');
        if ($postIdsOption) {
            $postIds = array_map('trim', explode(',', $postIdsOption));
            $postIds = array_filter($postIds, fn($id) => is_numeric($id));

            if (empty($postIds)) {
                $this->error('Invalid post IDs provided. Use comma-separated numeric IDs (e.g., --post-ids=29,30,405)');
                return Command::FAILURE;
            }

            $query->whereIn('id', $postIds);
            $this->info('Filtering by post IDs: ' . implode(', ', $postIds));
        }

        $totalPosts = $query->count();

        if ($totalPosts === 0) {
            $this->warn('No posts found to backup.');
            return Command::SUCCESS;
        }

        $this->info("Found {$totalPosts} posts to backup");

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

        // Track counts and media files
        $counts = ['posts' => 0, 'post_translations' => 0, 'meetings' => 0, 'media_files' => 0];
        $mediaFiles = [];

        // Write posts as JSON incrementally to a temp file to avoid holding everything in memory
        $tempJsonPath = storage_path('app/temp/' . uniqid('backup_json_') . '.json');
        $tempDir = dirname($tempJsonPath);
        if (!File::isDirectory($tempDir)) {
            File::makeDirectory($tempDir, 0755, true);
        }

        $jsonHandle = fopen($tempJsonPath, 'w');
        fwrite($jsonHandle, '{"meta":"__PLACEHOLDER__","posts":[');

        $bar = $this->output->createProgressBar($totalPosts);
        $bar->start();

        $isFirst = true;

        $query->with(['translations', 'meeting.location', 'media'])
            ->chunk(100, function ($posts) use (
                $categoryTypes, $excludeMedia, $jsonHandle,
                &$isFirst, &$counts, &$mediaFiles, $bar
            ) {
                foreach ($posts as $post) {
                    $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']++;
                    }

                    if (!$excludeMedia) {
                        $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']++;

                    $bar->advance();
                }
            });

        $bar->finish();
        $this->newLine();

        // 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' => !$excludeMedia,
            'counts' => $counts,
        ], JSON_UNESCAPED_UNICODE);

        // Replace the placeholder in the temp JSON file without reading it all into memory
        $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);

        if ($excludeMedia) {
            // Move temp JSON as the final output
            File::move($tempJsonPath, $outputPath);
        } else {
            // Create ZIP archive with JSON and media files
            $this->info('Creating ZIP archive with media files...');

            if (!class_exists('ZipArchive')) {
                @unlink($tempJsonPath);
                $this->error('ZipArchive extension is not available. Install php-zip extension or use --exclude-media flag.');
                return Command::FAILURE;
            }

            $zip = new ZipArchive();
            if ($zip->open($outputPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
                @unlink($tempJsonPath);
                $this->error("Failed to create ZIP archive: {$outputPath}");
                return Command::FAILURE;
            }

            // Add backup.json to archive
            $zip->addFile($tempJsonPath, 'backup.json');

            // Add media files to archive
            $mediaBar = $this->output->createProgressBar(count($mediaFiles));
            $mediaBar->start();

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

            $mediaBar->finish();
            $this->newLine();

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

        $fileSize = $this->formatBytes(File::size($outputPath));

        $this->info("Backup completed successfully!");
        $this->table(
            ['Metric', 'Value'],
            [
                ['Posts', $counts['posts']],
                ['Translations', $counts['post_translations']],
                ['Meetings', $counts['meetings']],
                ['Media Files', $counts['media_files']],
                ['File Size', $fileSize],
                ['Output File', $outputPath],
            ]
        );

        return Command::SUCCESS;
    }

    /**
     * Format bytes to human readable format.
     */
    private function formatBytes(int $bytes): string
    {
        $units = ['B', 'KB', 'MB', 'GB'];
        $i = 0;
        while ($bytes >= 1024 && $i < count($units) - 1) {
            $bytes /= 1024;
            $i++;
        }
        return round($bytes, 2) . ' ' . $units[$i];
    }

    /**
     * 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,
        ];
    }
}
