<?php

declare(strict_types=1);

namespace App\Console\Commands;

use App\Jobs\GenerateAiBiographyJob;
use App\Models\CreatorAiBiography;
use App\Models\User;
use App\Services\AiBiography\AiBiographyInputBuilder;
use App\Services\AiBiography\AiBiographyPromptBuilder;
use App\Services\AiBiography\AiBiographyService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;

/**
 * Generate AI biographies for one creator or refresh stale ones.
 *
 * Usage:
 *   php artisan ai-biography:generate {user_id}
 *   php artisan ai-biography:generate
 *   php artisan ai-biography:generate --stale
 *   php artisan ai-biography:generate --stale --limit=50 --queue
 */
final class GenerateAiBiographyCommand extends Command
{
    protected $signature = 'ai-biography:generate
                            {user_id? : The ID of a single creator to generate a biography for}
                            {--stale : Refresh all biographies whose source hash has changed}
                            {--all : Alias for generating only missing biographies ordered by latest public upload}
                            {--provider= : Override the configured LLM provider for this run (vision_gateway|vision|gemini|home)}
                            {--prompt : Output the initial prompt payload that would be sent for each processed creator}
                            {--result : Output the generated biography text to the console after a successful inline run}
                            {--skip-existing : Skip creators who already have an active AI biography}
                            {--limit=100 : Maximum number of creators to process in batch mode}
                            {--chunk=50 : Query chunk size for batch operations}
                            {--force : Overwrite user-edited biographies}
                            {--queue : Dispatch jobs to the queue instead of running inline}
                            {--dry-run : List candidates without generating}';

    protected $description = 'Generate missing AI biographies or refresh stale ones';

    public function handle(
        AiBiographyService $biographies,
        AiBiographyInputBuilder $inputBuilder,
        AiBiographyPromptBuilder $promptBuilder,
    ): int
    {
        if (! config('ai_biography.enabled', true)) {
            $this->warn('AI Biography is disabled (AI_BIOGRAPHY_ENABLED=false).');

            return self::SUCCESS;
        }

        $provider = $this->resolveProviderOverride();

        if ($provider === false) {
            return self::FAILURE;
        }

        if (is_string($provider)) {
            config(['ai_biography.provider_override' => $provider]);
            config(['ai_biography.provider' => $provider]);
            $this->line("Using AI biography provider override: {$provider}");
        }

        $userId  = $this->argument('user_id');
        $stale   = (bool) $this->option('stale');
        $all     = (bool) $this->option('all');
        $prompt  = (bool) $this->option('prompt');
        $result  = (bool) $this->option('result');
        $skipExisting = (bool) $this->option('skip-existing');
        $force   = (bool) $this->option('force');
        $queue   = (bool) $this->option('queue');
        $dryRun  = (bool) $this->option('dry-run');
        $limit   = max(1, (int) $this->option('limit'));
        $chunk   = max(1, (int) $this->option('chunk'));

        if ($userId !== null) {
            return $this->handleSingle((int) $userId, $biographies, $inputBuilder, $promptBuilder, $force, $queue, $dryRun, $provider ?: null, $prompt, $result, $skipExisting);
        }

        if ($stale) {
            return $this->handleStale($biographies, $inputBuilder, $promptBuilder, $force, $queue, $dryRun, $limit, $chunk, $provider ?: null, $prompt, $result, $skipExisting);
        }

        if ($all) {
            $this->line('`--all` is now an alias for the default missing-only batch mode.');
        }

        return $this->handleMissing($biographies, $inputBuilder, $promptBuilder, $force, $queue, $dryRun, $limit, $provider ?: null, $prompt, $result, $skipExisting);
    }

    private function resolveProviderOverride(): string|false|null
    {
        $rawProvider = $this->option('provider');

        if ($rawProvider === null || trim((string) $rawProvider) === '') {
            return null;
        }

        $provider = strtolower(trim((string) $rawProvider));

        return match ($provider) {
            'vision_gateway', 'vision', 'local' => 'vision_gateway',
            'gemini' => 'gemini',
            'home', 'lmstudio', 'lm_studio' => 'home',
            default => $this->invalidProvider($provider),
        };
    }

    private function invalidProvider(string $provider): false
    {
        $this->error("Invalid provider [{$provider}]. Supported values: vision_gateway, vision, gemini, home.");

        return false;
    }

    private function handleSingle(
        int $userId,
        AiBiographyService $biographies,
        AiBiographyInputBuilder $inputBuilder,
        AiBiographyPromptBuilder $promptBuilder,
        bool $force,
        bool $queue,
        bool $dryRun,
        ?string $provider,
        bool $showPrompt,
        bool $showResult,
        bool $skipExisting,
    ): int {
        $user = User::query()->where('id', $userId)->where('is_active', true)->whereNull('deleted_at')->first();

        if ($user === null) {
            $this->error("User #{$userId} not found or inactive.");

            return self::FAILURE;
        }

        $this->line("Processing user #{$userId} ({$user->username})");

        if ($skipExisting && $this->hasActiveBiography($userId)) {
            $this->info('  ↷ skipped_existing_active');

            return self::SUCCESS;
        }

        if ($showPrompt) {
            $this->outputPromptPreview($user, $inputBuilder, $promptBuilder);
        }

        if ($dryRun) {
            $this->info('[dry-run] Would generate biography.');

            return self::SUCCESS;
        }

        if ($queue) {
            if ($showResult) {
                $this->warn('[--result] is only available for inline runs. The job was queued, so no biography text is available yet.');
            }

            GenerateAiBiographyJob::dispatch($userId, $force, $provider)->onQueue((string) config('ai_biography.queue', 'default'));
            $this->info("Queued biography generation for #{$userId}.");

            return self::SUCCESS;
        }

        $result = $biographies->regenerate($user, $force);

        if ($result['success']) {
            $this->info("  ✓ {$result['action']}");
            if ($showResult) {
                $this->outputGeneratedBiography($userId);
            }
        } else {
            $this->warn("  ✗ {$result['action']}: " . implode(', ', $result['errors']));
        }

        return $result['success'] ? self::SUCCESS : self::FAILURE;
    }

    private function handleStale(
        AiBiographyService $biographies,
        AiBiographyInputBuilder $inputBuilder,
        AiBiographyPromptBuilder $promptBuilder,
        bool $force,
        bool $queue,
        bool $dryRun,
        int $limit,
        int $chunk,
        ?string $provider,
        bool $showPrompt,
        bool $showResult,
        bool $skipExisting,
    ): int {
        if ($skipExisting) {
            $this->warn('`--skip-existing` is ignored with `--stale` because stale refresh only applies to creators who already have an active AI biography.');
        }

        $this->info("Scanning for stale AI biographies (limit={$limit})...");

        $processed = 0;
        $queued    = 0;
        $generated = 0;
        $skipped   = 0;

        User::query()
            ->where('is_active', true)
            ->whereNull('deleted_at')
            ->whereExists(fn ($q) => $q->from('creator_ai_biographies')->whereColumn('creator_ai_biographies.user_id', 'users.id')->where('creator_ai_biographies.is_active', true))
            ->chunkById($chunk, function ($users) use ($biographies, $inputBuilder, $promptBuilder, $force, $queue, $dryRun, $limit, $provider, $showPrompt, $showResult, &$processed, &$queued, &$generated, &$skipped): bool {
                foreach ($users as $user) {
                    if ($processed >= $limit) {
                        return false;
                    }

                    if (! $biographies->isStale($user)) {
                        continue;
                    }

                    $processed++;
                    $this->line("  [{$user->id}] {$user->username} – stale");

                    if ($showPrompt) {
                        $this->outputPromptPreview($user, $inputBuilder, $promptBuilder, '    ');
                    }

                    if ($dryRun) {
                        $skipped++;
                        continue;
                    }

                    if ($queue) {
                        if ($showResult) {
                            $this->warn("    [--result] is only available for inline runs. User #{$user->id} was queued, so no biography text is available yet.");
                        }

                        GenerateAiBiographyJob::dispatch((int) $user->id, $force, $provider)->onQueue((string) config('ai_biography.queue', 'default'));
                        $queued++;
                    } else {
                        $result = $biographies->regenerate($user, $force);
                        if ($result['success']) {
                            $generated++;

                            if ($showResult) {
                                $this->outputGeneratedBiography((int) $user->id, '    ');
                            }
                        } else {
                            $skipped++;
                        }
                    }
                }

                return true;
            });

        $this->info("Done. processed={$processed}  queued={$queued}  generated={$generated}  skipped/dry={$skipped}");

        return self::SUCCESS;
    }

    private function handleMissing(
        AiBiographyService $biographies,
        AiBiographyInputBuilder $inputBuilder,
        AiBiographyPromptBuilder $promptBuilder,
        bool $force,
        bool $queue,
        bool $dryRun,
        int $limit,
        ?string $provider,
        bool $showPrompt,
        bool $showResult,
        bool $skipExisting,
    ): int {
        $this->info("Generating missing AI biographies ordered by latest public upload (limit={$limit})...");

        $processed = 0;
        $queued    = 0;
        $generated = 0;
        $skipped   = 0;

        $latestUploads = DB::table('artworks')
            ->selectRaw('user_id, MAX(published_at) as latest_uploaded_at')
            ->where('is_public', true)
            ->where('is_approved', true)
            ->whereNotNull('published_at')
            ->whereNull('deleted_at')
            ->groupBy('user_id');

        $users = User::query()
            ->leftJoinSub($latestUploads, 'latest_public_artwork', function ($join): void {
                $join->on('latest_public_artwork.user_id', '=', 'users.id');
            })
            ->select('users.*')
            ->where('users.is_active', true)
            ->whereNull('users.deleted_at')
            ->whereNotExists(fn ($q) => $q->from('creator_ai_biographies')->whereColumn('creator_ai_biographies.user_id', 'users.id')->where('creator_ai_biographies.is_active', true))
            ->orderByDesc('latest_public_artwork.latest_uploaded_at')
            ->orderByDesc('users.id')
            ->limit($limit)
            ->get();

        foreach ($users as $user) {
            $processed++;
            $this->line("  [{$user->id}] {$user->username}");

            if ($showPrompt) {
                $this->outputPromptPreview($user, $inputBuilder, $promptBuilder, '    ');
            }

            if ($dryRun) {
                $skipped++;
                continue;
            }

            if ($queue) {
                if ($showResult) {
                    $this->warn("    [--result] is only available for inline runs. User #{$user->id} was queued, so no biography text is available yet.");
                }

                GenerateAiBiographyJob::dispatch((int) $user->id, $force, $provider)->onQueue((string) config('ai_biography.queue', 'default'));
                $queued++;
            } else {
                $result = $biographies->generate($user);
                if ($result['success']) {
                    $generated++;

                    if ($showResult) {
                        $this->outputGeneratedBiography((int) $user->id, '    ');
                    }
                } else {
                    $skipped++;
                }
            }
        }

        $this->info("Done. processed={$processed}  queued={$queued}  generated={$generated}  skipped/dry={$skipped}");

        return self::SUCCESS;
    }

    private function outputPromptPreview(
        User $user,
        AiBiographyInputBuilder $inputBuilder,
        AiBiographyPromptBuilder $promptBuilder,
        string $indent = '  ',
    ): void {
        $input = $inputBuilder->build($user);
        $qualityTier = $inputBuilder->qualityTier($input);
        $meetsThreshold = $inputBuilder->meetsMinimumThreshold($input);

        $this->line($indent . 'Prompt preview');
        $this->line($indent . '  Provider       : ' . $this->resolvedProvider());
        $this->line($indent . '  Quality tier   : ' . $qualityTier);
        $this->line($indent . '  Meets threshold: ' . ($meetsThreshold ? 'yes' : 'no'));

        if (! $meetsThreshold) {
            $this->line($indent . '  No prompt will be sent because this profile is below the minimum generation threshold.');

            return;
        }

        $payload = $promptBuilder->build($input, strict: false, sparse: $qualityTier === 'sparse');
        $systemPrompt = (string) ($payload['messages'][0]['content'] ?? '');
        $userPrompt = (string) ($payload['messages'][1]['content'] ?? '');

        $this->line($indent . '  Prompt version : ' . (string) ($payload['prompt_version'] ?? AiBiographyPromptBuilder::PROMPT_VERSION));
        $this->line($indent . '  Max tokens     : ' . (string) ($payload['max_tokens'] ?? 'n/a'));
        $this->line($indent . '  Temperature    : ' . (string) ($payload['temperature'] ?? 'n/a'));
        $this->line($indent . '  System prompt:');
        $this->writeIndentedBlock($systemPrompt, $indent . '    ');
        $this->line($indent . '  User prompt:');
        $this->writeIndentedBlock($userPrompt, $indent . '    ');
    }

    private function writeIndentedBlock(string $text, string $indent): void
    {
        foreach (preg_split("/\r\n|\r|\n/", trim($text)) ?: [] as $line) {
            $this->line($indent . $line);
        }
    }

    private function resolvedProvider(): string
    {
        $override = trim(strtolower((string) config('ai_biography.provider_override', '')));

        if (in_array($override, ['together', 'vision_gateway', 'gemini', 'home'], true)) {
            return $override;
        }

        if (trim((string) config('ai_biography.together.api_key', '')) !== ''
            && trim((string) config('ai_biography.together.model', '')) !== '') {
            return 'together';
        }

        $provider = trim(strtolower((string) config('ai_biography.provider', 'together')));

        return in_array($provider, ['together', 'vision_gateway', 'gemini', 'home'], true) ? $provider : 'together';
    }

    private function outputGeneratedBiography(int $userId, string $indent = '  '): void
    {
        $record = CreatorAiBiography::query()
            ->where('user_id', $userId)
            ->latest('id')
            ->first();

        $text = trim((string) ($record?->text ?? ''));

        if ($text === '') {
            $this->line($indent . 'Generated biography text: n/a');
            return;
        }

        $this->line($indent . 'Generated biography text:');
        foreach (preg_split('/\r\n|\r|\n/', $text) ?: [] as $line) {
            $this->line($indent . '  ' . $line);
        }
    }

    private function hasActiveBiography(int $userId): bool
    {
        return CreatorAiBiography::query()
            ->where('user_id', $userId)
            ->where('is_active', true)
            ->exists();
    }
}
