<?php

declare(strict_types=1);

namespace App\Console\Commands\Enhance;

use App\Models\EnhanceJob;
use App\Services\Enhance\EnhanceProcessorFactory;
use App\Services\Enhance\EnhanceStorageService;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Throwable;

final class EnhanceRunCommand extends Command
{
    protected $signature = 'enhance:run
        {--id= : Process specific job ID(s), comma-separated}
        {--limit=1 : Max pending/queued jobs to pick up from the queue (0 = all)}
        {--engine= : Override the processing engine for this run (stub, external_worker)}
        {--failed : Also include failed jobs when scanning the queue}
        {--dry-run : Show what would be processed without executing}';

    protected $description = 'Synchronously process pending enhance jobs inline — useful for debugging with -v / -vv / -vvv.';

    private const PROCESSABLE_STATUSES = [
        EnhanceJob::STATUS_PENDING,
        EnhanceJob::STATUS_QUEUED,
        EnhanceJob::STATUS_PROCESSING,
        EnhanceJob::STATUS_FAILED,
    ];

    public function __construct(
        private readonly EnhanceProcessorFactory $processorFactory,
        private readonly EnhanceStorageService $storage,
    ) {
        parent::__construct();
    }

    public function handle(): int
    {
        $dryRun = (bool) $this->option('dry-run');
        $engineOverride = trim((string) $this->option('engine'));
        $idOption = trim((string) $this->option('id'));
        $limit = max(0, (int) $this->option('limit'));
        $includeFailed = (bool) $this->option('failed');

        if ($engineOverride !== '' && ! in_array($engineOverride, [EnhanceJob::ENGINE_STUB, EnhanceJob::ENGINE_EXTERNAL_WORKER], true)) {
            $this->error("Unknown engine override: {$engineOverride}. Use 'stub' or 'external_worker'.");

            return self::FAILURE;
        }

        $jobs = $this->resolveJobs($idOption, $limit, $includeFailed);

        if ($jobs->isEmpty()) {
            $this->info('No eligible enhance jobs found.');

            return self::SUCCESS;
        }

        $this->info(sprintf('Found %d job(s) to process.', $jobs->count()));
        $this->newLine();

        if ($dryRun) {
            $this->warn('Dry-run mode — no jobs will be processed.');

            foreach ($jobs as $job) {
                $engine = $engineOverride !== '' ? "{$engineOverride} (overridden)" : $job->engine;
                $this->line(sprintf(
                    '  [dry-run] Job #%d  status=%-12s engine=%-18s scale=%dx  mode=%-14s user_id=%d',
                    $job->id,
                    $job->status,
                    $engine,
                    $job->scale,
                    $job->mode,
                    $job->user_id,
                ));
            }

            return self::SUCCESS;
        }

        $processed = 0;
        $failed = 0;

        foreach ($jobs as $job) {
            if ($this->processJob($job, $engineOverride)) {
                $processed++;
            } else {
                $failed++;
            }

            $this->newLine();
        }

        $this->info(sprintf('Done: %d completed, %d failed.', $processed, $failed));

        return $failed > 0 ? self::FAILURE : self::SUCCESS;
    }

    private function resolveJobs(string $idOption, int $limit, bool $includeFailed): Collection
    {
        if ($idOption !== '') {
            $ids = array_filter(array_map('intval', explode(',', $idOption)));

            return EnhanceJob::query()
                ->whereIn('id', $ids)
                ->whereIn('status', self::PROCESSABLE_STATUSES)
                ->get();
        }

        $statuses = [EnhanceJob::STATUS_PENDING, EnhanceJob::STATUS_QUEUED, EnhanceJob::STATUS_PROCESSING];

        if ($includeFailed) {
            $statuses[] = EnhanceJob::STATUS_FAILED;
        }

        $query = EnhanceJob::query()->whereIn('status', $statuses)->oldest();

        if ($limit > 0) {
            $query->limit($limit);
        }

        return $query->get();
    }

    private function processJob(EnhanceJob $job, string $engineOverride): bool
    {
        $engine = $engineOverride !== '' ? $engineOverride : (string) $job->engine;

        $this->line(sprintf('<comment>--- Job #%d ---</comment>', $job->id));
        $this->line(sprintf('  Status  : %s', $job->status));
        $this->line(sprintf('  Engine  : %s%s', $engine, $engineOverride !== '' ? ' (overridden)' : ''));
        $this->line(sprintf('  Scale   : %dx', $job->scale));
        $this->line(sprintf('  Mode    : %s', $job->mode));
        $this->line(sprintf('  User    : #%d', $job->user_id));

        if ($this->output->isVerbose()) {
            $this->line(sprintf(
                '  Source  : disk=%-10s path=%s',
                $job->source_disk ?: '(default)',
                $job->source_path ?: '—',
            ));
            $this->line(sprintf(
                '  Input   : %dx%d  size=%s  mime=%s',
                (int) $job->input_width,
                (int) $job->input_height,
                $this->formatBytes((int) $job->input_filesize),
                $job->input_mime ?: '—',
            ));

            if ($job->error_message !== null) {
                $this->warn(sprintf('  Previous error: %s', $job->error_message));
            }
        }

        if (! in_array($job->status, self::PROCESSABLE_STATUSES, true)) {
            $this->warn(sprintf('  Skipping: status "%s" is not processable.', $job->status));

            return false;
        }

        $job->forceFill([
            'status' => EnhanceJob::STATUS_PROCESSING,
            'started_at' => now(),
            'finished_at' => null,
            'error_message' => null,
        ])->save();

        $started = microtime(true);
        $completedExpiryDays = (int) config('enhance.lifecycle.completed_expires_after_days', 30);

        try {
            $this->line('  Processing...');

            $processor = $this->processorFactory->make($engine);
            $result = $processor->process($job);

            if ($this->output->isVerbose()) {
                $this->line(sprintf(
                    '  Output  : %dx%d  size=%s  mime=%s',
                    $result->width,
                    $result->height,
                    $this->formatBytes($result->filesize),
                    $result->mime,
                ));
                $this->line(sprintf(
                    '  Stored  : disk=%-10s path=%s',
                    $result->disk,
                    $result->path,
                ));
            }

            $this->line('  Generating preview...');
            $preview = $this->storage->createPreviewFromStoredOutput($job, $result->disk, $result->path) ?? [];

            $outputHash = null;
            $outputContents = Storage::disk($result->disk)->get($result->path);

            if (is_string($outputContents) && $outputContents !== '') {
                $outputHash = hash('sha256', $outputContents);
            }

            $job->forceFill([
                'status' => EnhanceJob::STATUS_COMPLETED,
                'output_disk' => $result->disk,
                'output_path' => $result->path,
                'output_hash' => $outputHash,
                'output_width' => $result->width,
                'output_height' => $result->height,
                'output_filesize' => $result->filesize,
                'output_mime' => $result->mime,
                'metadata' => array_merge($job->metadata ?? [], $result->metadata ?? []),
                'processing_seconds' => (int) round(microtime(true) - $started),
                'finished_at' => now(),
                'expires_at' => $completedExpiryDays > 0 ? now()->addDays($completedExpiryDays) : null,
            ] + $preview)->save();

            $elapsed = round(microtime(true) - $started, 2);
            $this->info(sprintf('  Completed in %.2fs', $elapsed));

            if ($this->output->isVerbose() && ! empty($result->metadata)) {
                $this->line('  Metadata:');

                foreach ($result->metadata as $key => $value) {
                    $display = is_scalar($value) ? (string) $value : json_encode($value);
                    $this->line(sprintf('    %-30s %s', $key . ':', $display));
                }
            }

            return true;
        } catch (Throwable $exception) {
            $elapsed = round(microtime(true) - $started, 2);

            $job->forceFill([
                'status' => EnhanceJob::STATUS_FAILED,
                'error_message' => Str::limit($exception->getMessage(), 1000),
                'processing_seconds' => (int) round(microtime(true) - $started),
                'finished_at' => now(),
            ])->save();

            $this->error(sprintf('  Failed in %.2fs: %s', $elapsed, $exception->getMessage()));

            if ($this->output->isVerbose()) {
                $this->line(sprintf('  Exception : %s', get_class($exception)));
                $this->line(sprintf('  At        : %s:%d', $exception->getFile(), $exception->getLine()));

                $previous = $exception->getPrevious();

                if ($previous !== null) {
                    $this->line(sprintf('  Caused by : %s: %s', get_class($previous), $previous->getMessage()));
                }
            }

            if ($this->output->isVeryVerbose()) {
                $this->line('  Stack trace:');
                $frames = array_slice(explode("\n", $exception->getTraceAsString()), 0, 25);

                foreach ($frames as $frame) {
                    $this->line('    ' . $frame);
                }
            }

            Log::warning('enhance.run.command.failed', [
                'enhance_job_id' => $job->id,
                'engine' => $engine,
                'message' => $exception->getMessage(),
                'exception' => get_class($exception),
            ]);

            return false;
        }
    }

    private function formatBytes(int $bytes): string
    {
        if ($bytes < 1024) {
            return $bytes . 'B';
        }

        if ($bytes < 1_048_576) {
            return round($bytes / 1024, 1) . 'KB';
        }

        return round($bytes / 1_048_576, 1) . 'MB';
    }
}
