<?php

declare(strict_types=1);

namespace App\Console\Commands;

use App\Facades\Elasticsearch;
use App\Models\Release;
use App\Services\Search\Support\ElasticsearchResponseHelper;
use Elastic\Elasticsearch\Client as ElasticsearchClient;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class UpdateReleasesIndexSchemaES extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'nntmux:update-releases-index-es
                                        {--add-fields : Add new media-related fields to releases index}
                                        {--update-media-ids : Update existing indexed releases with media IDs from database}
                                        {--batch-size=1000 : Batch size for bulk update operations}
                                        {--movies-only : Only update releases with movie info}
                                        {--tv-only : Only update releases with TV show info}
                                        {--missing-only : Only update releases that have zero media IDs in index}
                                        {--force : Force schema update even if fields exist}
                                        {--recreate-index : Recreate the index with new schema (will delete existing data)}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Update ElasticSearch releases index schema with new media fields and/or populate media IDs for existing releases';

    /**
     * The media fields that should exist in the releases index
     *
     * @var array<string, mixed>
     */
    private array $mediaFields = [
        'imdbid' => ['type' => 'keyword'],
        'tmdbid' => ['type' => 'integer'],
        'traktid' => ['type' => 'integer'],
        'tvdb' => ['type' => 'integer'],
        'tvmaze' => ['type' => 'integer'],
        'tvrage' => ['type' => 'integer'],
        'videos_id' => ['type' => 'integer'],
        'movieinfo_id' => ['type' => 'integer'],
        'media_movie_name' => ['type' => 'text', 'analyzer' => 'release_analyzer'],
        'media_file_name' => ['type' => 'text', 'analyzer' => 'release_analyzer'],
        'media_unique_id' => ['type' => 'keyword', 'ignore_above' => 256],
        'media_container_format' => ['type' => 'text', 'analyzer' => 'release_analyzer'],
        'media_video_format' => ['type' => 'text', 'analyzer' => 'release_analyzer'],
        'media_video_codec' => ['type' => 'text', 'analyzer' => 'release_analyzer'],
        'media_video_width' => ['type' => 'integer'],
        'media_video_height' => ['type' => 'integer'],
        'media_audio_format' => ['type' => 'text', 'analyzer' => 'release_analyzer'],
        'media_audio_channels' => ['type' => 'text', 'analyzer' => 'release_analyzer'],
        'media_audio_language' => ['type' => 'text', 'analyzer' => 'release_analyzer'],
        'media_subtitle_language' => ['type' => 'text', 'analyzer' => 'release_analyzer'],
        'has_media_info' => ['type' => 'integer'],
    ];

    private ?ElasticsearchClient $client = null;

    /**
     * Execute the console command.
     */
    public function handle(): int
    {
        $driver = config('search.default', 'manticore');

        if ($driver !== 'elasticsearch') {
            $this->warn("Current search driver is '{$driver}'. This command is for ElasticSearch.");
            if (! $this->confirm('Do you want to continue anyway?', false)) {
                return Command::SUCCESS;
            }
        }

        $this->info('ElasticSearch releases index schema update utility');
        $this->newLine();

        // Test connection
        try {
            $health = Elasticsearch::cluster()->health();
            $this->info("Connected to ElasticSearch. Cluster status: {$health['status']}");
        } catch (\Exception $e) {
            $this->error('Failed to connect to ElasticSearch: '.$e->getMessage());

            return Command::FAILURE;
        }

        $result = Command::SUCCESS;

        // Handle recreate-index option
        if ($this->option('recreate-index')) {
            $result = $this->recreateIndexWithNewSchema();
            if ($result !== Command::SUCCESS) {
                return $result;
            }
        }

        // Handle add-fields option
        if ($this->option('add-fields')) {
            $result = $this->addNewFields();
            if ($result !== Command::SUCCESS) {
                return $result;
            }
        }

        // Handle update-media-ids option
        if ($this->option('update-media-ids')) {
            $result = $this->updateMediaIds();
        }

        // If no options specified, show current schema info
        if (! $this->option('add-fields') && ! $this->option('update-media-ids') && ! $this->option('recreate-index')) {
            $this->showSchemaInfo();
        }

        return $result;
    }

    private function elasticsearchClient(): ElasticsearchClient
    {
        if ($this->client instanceof ElasticsearchClient) {
            return $this->client;
        }

        /** @var ElasticsearchClient $client */
        $client = app('elasticsearch');
        $this->client = $client;

        return $this->client;
    }

    /**
     * Show current index schema information
     */
    private function showSchemaInfo(): void
    {
        $this->info('Current releases index schema:');
        $this->newLine();

        try {
            $indexName = config('search.drivers.elasticsearch.indexes.releases', 'releases');

            if (! ElasticsearchResponseHelper::boolResponse($this->elasticsearchClient(), fn (ElasticsearchClient $client) => $client->indices()->exists(['index' => $indexName]))) {
                $this->warn("Index '{$indexName}' does not exist.");
                $this->info('Run `php artisan nntmux:create-es-indexes` to create the index first.');

                return;
            }

            $mapping = Elasticsearch::indices()->getMapping(['index' => $indexName]);
            $properties = $mapping[$indexName]['mappings']['properties'] ?? [];

            if (empty($properties)) {
                $this->warn('Index has no mapped properties.');

                return;
            }

            $headers = ['Field', 'Type', 'Properties'];
            $rows = [];
            $existingFields = [];

            foreach ($properties as $field => $config) {
                $type = $config['type'] ?? 'unknown';
                $props = [];
                if (isset($config['analyzer'])) {
                    $props[] = "analyzer: {$config['analyzer']}";
                }
                if (isset($config['index']) && ! $config['index']) {
                    $props[] = 'not indexed';
                }
                if (isset($config['fields'])) {
                    $props[] = 'multi-field';
                }
                $rows[] = [$field, $type, implode(', ', $props)];
                $existingFields[$field] = $type;
            }

            $this->table($headers, $rows);
            $this->newLine();

            // Check for missing media fields
            $missingFields = [];
            foreach ($this->mediaFields as $field => $config) {
                if (! isset($existingFields[$field])) {
                    $missingFields[] = $field;
                }
            }

            if (! empty($missingFields)) {
                $this->warn('Missing media fields that should be added:');
                foreach ($missingFields as $field) {
                    $this->line("  - {$field} ({$this->mediaFields[$field]['type']})");
                }
                $this->newLine();
                $this->info('Run with --add-fields to add missing fields, or --recreate-index to rebuild with new schema.');
            } else {
                $this->info('All expected media fields are present in the index.');
            }

            // Show usage hints
            $this->newLine();
            $this->info('Available options:');
            $this->line('  --add-fields         Add missing media-related fields to the index');
            $this->line('  --recreate-index     Recreate index with new schema (data will be lost)');
            $this->line('  --update-media-ids   Update indexed releases with media IDs from database');
            $this->line('  --movies-only        Only update releases with movie info');
            $this->line('  --tv-only            Only update releases with TV show info');
            $this->line('  --missing-only       Only update releases with zero media IDs');
            $this->line('  --batch-size=N       Set batch size for updates (default: 1000)');

        } catch (\Throwable $e) {
            $this->error('Failed to get index mapping: '.$e->getMessage());
            $this->info('The index may not exist. Run `php artisan nntmux:create-es-indexes` first.');
        }
    }

    /**
     * Add new fields to the releases index using mapping update
     *
     * Note: Elasticsearch only allows adding new fields, not modifying existing ones
     */
    private function addNewFields(): int
    {
        $this->info('Checking for missing fields in releases index...');

        try {
            $indexName = config('search.drivers.elasticsearch.indexes.releases', 'releases');

            if (! ElasticsearchResponseHelper::boolResponse($this->elasticsearchClient(), fn (ElasticsearchClient $client) => $client->indices()->exists(['index' => $indexName]))) {
                $this->error("Index '{$indexName}' does not exist. Create it first with: php artisan nntmux:create-es-indexes");

                return Command::FAILURE;
            }

            // Get current mapping
            $mapping = Elasticsearch::indices()->getMapping(['index' => $indexName]);
            $existingProperties = $mapping[$indexName]['mappings']['properties'] ?? [];

            // Find fields to add
            $fieldsToAdd = [];
            foreach ($this->mediaFields as $field => $config) {
                if (! isset($existingProperties[$field]) || $this->option('force')) {
                    $fieldsToAdd[$field] = $config;
                }
            }

            if (empty($fieldsToAdd)) {
                $this->info('All media fields already exist in the index.');

                return Command::SUCCESS;
            }

            $this->warn('The following fields will be added:');
            foreach ($fieldsToAdd as $field => $config) {
                $this->line("  - {$field} ({$config['type']})");
            }

            if (! $this->confirm('Do you want to proceed with adding these fields?', true)) {
                $this->info('Operation cancelled.');

                return Command::SUCCESS;
            }

            // Add new fields via PUT mapping
            $newProperties = [];
            foreach ($fieldsToAdd as $field => $config) {
                $newProperties[$field] = $config;
            }

            Elasticsearch::indices()->putMapping([
                'index' => $indexName,
                'body' => [
                    'properties' => $newProperties,
                ],
            ]);

            $this->info('Schema update completed successfully!');
            $this->newLine();
            $this->info('Now you can update existing releases with media IDs using:');
            $this->line('  php artisan nntmux:update-releases-index-es --update-media-ids');

            return Command::SUCCESS;

        } catch (\Throwable $e) {
            $this->error('Failed to update schema: '.$e->getMessage());

            return Command::FAILURE;
        }
    }

    /**
     * Recreate the index with the new schema including media fields
     * WARNING: This will delete all existing data in the index
     */
    private function recreateIndexWithNewSchema(): int
    {
        $indexName = config('search.drivers.elasticsearch.indexes.releases', 'releases');

        $this->warn("WARNING: This will DELETE all data in the '{$indexName}' index and recreate it with the new schema!");
        $this->warn('You will need to re-populate the index after this operation.');

        if (! $this->confirm('Are you sure you want to proceed?', false)) {
            $this->info('Operation cancelled.');

            return Command::SUCCESS;
        }

        try {
            // Delete existing index if it exists
            if (ElasticsearchResponseHelper::boolResponse($this->elasticsearchClient(), fn (ElasticsearchClient $client) => $client->indices()->exists(['index' => $indexName]))) {
                $this->info("Deleting existing '{$indexName}' index...");
                Elasticsearch::indices()->delete(['index' => $indexName]);
            }

            // Create new index with updated schema
            $this->info("Creating '{$indexName}' index with new schema...");

            $releasesIndex = [
                'index' => $indexName,
                'body' => [
                    'settings' => [
                        'number_of_shards' => 2,
                        'number_of_replicas' => 0,
                        'analysis' => [
                            'analyzer' => [
                                'release_analyzer' => [
                                    'type' => 'custom',
                                    'tokenizer' => 'standard',
                                    'filter' => ['lowercase', 'asciifolding'],
                                ],
                            ],
                        ],
                    ],
                    'mappings' => [
                        'properties' => [
                            'id' => [
                                'type' => 'long',
                                'index' => false,
                            ],
                            'name' => [
                                'type' => 'text',
                                'analyzer' => 'release_analyzer',
                            ],
                            'searchname' => [
                                'type' => 'text',
                                'analyzer' => 'release_analyzer',
                                'fields' => [
                                    'keyword' => [
                                        'type' => 'keyword',
                                        'ignore_above' => 256,
                                    ],
                                    'sort' => [
                                        'type' => 'keyword',
                                    ],
                                ],
                            ],
                            'plainsearchname' => [
                                'type' => 'text',
                                'analyzer' => 'release_analyzer',
                                'fields' => [
                                    'keyword' => [
                                        'type' => 'keyword',
                                        'ignore_above' => 256,
                                    ],
                                    'sort' => [
                                        'type' => 'keyword',
                                    ],
                                ],
                            ],
                            'categories_id' => [
                                'type' => 'integer',
                            ],
                            'fromname' => [
                                'type' => 'text',
                                'analyzer' => 'release_analyzer',
                            ],
                            'filename' => [
                                'type' => 'text',
                                'analyzer' => 'release_analyzer',
                            ],
                            'add_date' => [
                                'type' => 'date',
                                'format' => 'yyyy-MM-dd HH:mm:ss',
                            ],
                            'post_date' => [
                                'type' => 'date',
                                'format' => 'yyyy-MM-dd HH:mm:ss',
                            ],
                            ...$this->mediaFields,
                        ],
                    ],
                ],
            ];

            Elasticsearch::indices()->create($releasesIndex);

            $this->info("Index '{$indexName}' created successfully with new schema!");
            $this->newLine();
            $this->warn('Remember to re-populate the index:');
            $this->line('  php artisan nntmux:populate-search-indexes --elastic --releases');

            return Command::SUCCESS;

        } catch (\Throwable $e) {
            $this->error('Failed to recreate index: '.$e->getMessage());

            return Command::FAILURE;
        }
    }

    /**
     * Update existing indexed releases with media IDs from database
     */
    private function updateMediaIds(): int
    {
        $this->info('Updating indexed releases with media IDs from database...');

        $batchSize = (int) $this->option('batch-size');
        $moviesOnly = $this->option('movies-only');
        $tvOnly = $this->option('tv-only');
        $missingOnly = $this->option('missing-only');

        $indexName = config('search.drivers.elasticsearch.indexes.releases', 'releases');

        // Check if index exists
        if (! ElasticsearchResponseHelper::boolResponse($this->elasticsearchClient(), fn (ElasticsearchClient $client) => $client->indices()->exists(['index' => $indexName]))) {
            $this->error("Index '{$indexName}' does not exist. Create it first.");

            return Command::FAILURE;
        }

        // Build query based on options
        $query = Release::query()
            ->leftJoin('movieinfo', 'releases.movieinfo_id', '=', 'movieinfo.id')
            ->leftJoin('videos', 'releases.videos_id', '=', 'videos.id')
            ->select([
                'releases.id',
                'releases.videos_id',
                'releases.movieinfo_id',
                // Movie external IDs
                'movieinfo.imdbid',
                'movieinfo.tmdbid',
                'movieinfo.traktid',
                // TV show external IDs
                'videos.tvdb',
                'videos.tvmaze',
                'videos.tvrage',
                DB::raw('videos.trakt as video_trakt'),
                DB::raw('videos.imdb as video_imdb'),
                DB::raw('videos.tmdb as video_tmdb'),
            ]);

        // Apply filters based on options
        if ($moviesOnly) {
            $query->whereNotNull('releases.movieinfo_id')
                ->where('releases.movieinfo_id', '>', 0);
            $this->info('Filtering: Movies only (releases with movieinfo_id)');
        } elseif ($tvOnly) {
            $query->whereNotNull('releases.videos_id')
                ->where('releases.videos_id', '>', 0);
            $this->info('Filtering: TV shows only (releases with videos_id)');
        } else {
            // Get releases that have either movie or TV info
            $query->where(function ($q) {
                $q->where(function ($subq) {
                    $subq->whereNotNull('releases.movieinfo_id')
                        ->where('releases.movieinfo_id', '>', 0);
                })->orWhere(function ($subq) {
                    $subq->whereNotNull('releases.videos_id')
                        ->where('releases.videos_id', '>', 0);
                });
            });
            $this->info('Filtering: Releases with either movie or TV info');
        }

        $total = $query->count();

        if ($total === 0) {
            $this->warn('No releases found matching the criteria.');

            return Command::SUCCESS;
        }

        $this->info("Found {$total} releases to update.");

        if (! $this->confirm('Do you want to proceed with the update?', true)) {
            $this->info('Operation cancelled.');

            return Command::SUCCESS;
        }

        $bar = $this->output->createProgressBar($total);
        $bar->setFormat(' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%');
        $bar->start();

        $updated = 0;
        $errors = 0;
        $skipped = 0;

        // Process in batches using chunk
        $query->orderBy('releases.id')
            ->chunk($batchSize, function ($releases) use (&$updated, &$errors, &$skipped, $indexName, $bar, $missingOnly) {
                $bulkParams = ['body' => []];

                foreach ($releases as $release) {
                    // Prepare the update data
                    $mediaData = $this->prepareMediaData($release);

                    // Skip if all media IDs are zero
                    $hasMediaIds = array_filter($mediaData, fn ($v) => $v > 0);
                    if (empty($hasMediaIds)) {
                        $skipped++;
                        $bar->advance();

                        continue;
                    }

                    // If missing-only, check if the document already has media IDs
                    if ($missingOnly && $this->documentHasMediaIds($indexName, $release->id)) {
                        $skipped++;
                        $bar->advance();

                        continue;
                    }

                    // Add to bulk update
                    $bulkParams['body'][] = [
                        'update' => [
                            '_index' => $indexName,
                            '_id' => $release->id,
                        ],
                    ];
                    $bulkParams['body'][] = [
                        'doc' => $mediaData,
                        'doc_as_upsert' => false, // Don't create if doesn't exist
                    ];

                    $bar->advance();
                }

                // Execute bulk update
                if (! empty($bulkParams['body'])) {
                    try {
                        $response = Elasticsearch::bulk($bulkParams);

                        if (isset($response['errors']) && $response['errors']) {
                            foreach ($response['items'] as $item) {
                                if (isset($item['update']['error'])) {
                                    $errors++;
                                    if ($errors <= 5) {
                                        Log::warning('Failed to update release in ES: '.json_encode($item['update']['error']));
                                    }
                                } else {
                                    $updated++;
                                }
                            }
                        } else {
                            $updated += count($response['items']);
                        }
                    } catch (\Throwable $e) {
                        $errors += count($bulkParams['body']) / 2;
                        if ($errors <= 5) {
                            Log::error('Bulk update failed: '.$e->getMessage());
                        }
                    }
                }
            });

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

        $this->info('Update completed!');
        $this->line("  - Updated: {$updated}");
        $this->line("  - Skipped: {$skipped}");
        if ($errors > 0) {
            $this->warn("  - Errors: {$errors}");
        }

        // Refresh index to make changes visible
        $this->info('Refreshing index...');
        Elasticsearch::indices()->refresh(['index' => $indexName]);
        $this->info('Done!');

        return Command::SUCCESS;
    }

    /**
     * Prepare media data for a release
     *
     * @return array<string, mixed>
     */
    private function prepareMediaData(mixed $release): array
    {
        return [
            'imdbid' => (string) ($release->imdbid ?: ''),
            'tmdbid' => (int) ($release->tmdbid ?: ($release->video_tmdb ?: 0)),
            'traktid' => (int) ($release->traktid ?: ($release->video_trakt ?: 0)),
            'tvdb' => (int) ($release->tvdb ?: 0),
            'tvmaze' => (int) ($release->tvmaze ?: 0),
            'tvrage' => (int) ($release->tvrage ?: 0),
            'videos_id' => (int) ($release->videos_id ?: 0),
            'movieinfo_id' => (int) ($release->movieinfo_id ?: 0),
        ];
    }

    /**
     * Check if a document already has media IDs in the index
     */
    private function documentHasMediaIds(string $indexName, int $id): bool
    {
        try {
            $doc = Elasticsearch::get([
                'index' => $indexName,
                'id' => $id,
                '_source' => ['imdbid', 'tmdbid', 'traktid', 'tvdb', 'tvmaze', 'tvrage'],
            ]);

            $source = $doc['_source'] ?? [];

            // Check if any media ID is non-zero
            return ($source['imdbid'] ?? 0) > 0
                || ($source['tmdbid'] ?? 0) > 0
                || ($source['traktid'] ?? 0) > 0
                || ($source['tvdb'] ?? 0) > 0
                || ($source['tvmaze'] ?? 0) > 0
                || ($source['tvrage'] ?? 0) > 0;

        } catch (\Throwable $e) {
            // Other error, assume no media IDs
            return false;
        }
    }
}
