<?php

declare(strict_types=1);

namespace App\Console\Commands\Enhance;

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

final class CleanupEnhanceJobsCommand extends Command
{
    protected $signature = 'enhance:cleanup
        {--dry-run : Preview cleanup actions only}
        {--force : Delete files and update records}
        {--only= : Restrict cleanup to expired, deleted, failed, or orphaned}
        {--days= : Override retention days for failed or deleted cleanup}';

    protected $description = 'Safely clean expired, deleted, failed, and orphaned Enhance files.';

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

    public function handle(): int
    {
        $target = strtolower(trim((string) $this->option('only')));
        $validTargets = ['', 'expired', 'deleted', 'failed', 'orphaned'];

        if (! in_array($target, $validTargets, true)) {
            $this->error('The --only option must be one of: expired, deleted, failed, orphaned.');

            return self::FAILURE;
        }

        if ((bool) $this->option('dry-run') && (bool) $this->option('force')) {
            $this->error('Use either --dry-run or --force, not both.');

            return self::FAILURE;
        }

        $dryRun = (bool) $this->option('dry-run') || ! (bool) $this->option('force');
        $daysOverride = $this->option('days');
        $selectedTarget = $target !== '' ? $target : 'all';

        Log::info('enhance.cleanup.started', [
            'dry_run' => $dryRun,
            'target' => $selectedTarget,
            'days_override' => $daysOverride,
        ]);

        if ($dryRun) {
            $this->warn('Running in dry-run mode. No files will be deleted.');
        }

        $rows = [];

        if ($target === '' || $target === 'expired') {
            $expired = $this->cleanupExpiredCompletedJobs($dryRun);
            $rows[] = ['expired', $expired['jobs'], $expired['files'], $dryRun ? 'dry-run' : 'cleaned'];
        }

        if ($target === '' || $target === 'failed') {
            $failed = $this->cleanupFailedJobs($dryRun, $daysOverride);
            $rows[] = ['failed', $failed['jobs'], $failed['files'], $dryRun ? 'dry-run' : 'cleaned'];
        }

        if ($target === '' || $target === 'deleted') {
            $deleted = $this->cleanupSoftDeletedJobs($dryRun, $daysOverride);
            $rows[] = ['deleted', $deleted['jobs'], $deleted['files'], $dryRun ? 'dry-run' : 'cleaned'];
        }

        if ($target === 'orphaned') {
            $orphaned = $this->scanOrphanedFiles($dryRun);
            $rows[] = ['orphaned', $orphaned['files'], $orphaned['deleted'], $dryRun ? 'dry-run' : 'deleted'];

            if ($orphaned['unsupported']) {
                $this->warn('Orphaned file scan was skipped because the configured disk does not support safe listing.');
            }

            foreach ($orphaned['sample'] as $path) {
                $this->line(' - ' . $path);
            }
        }

        if ($rows !== []) {
            $this->table(['Target', 'Jobs/Files', 'Files deleted', 'Mode'], $rows);
        }

        Log::info('enhance.cleanup.completed', [
            'dry_run' => $dryRun,
            'target' => $selectedTarget,
            'rows' => $rows,
        ]);

        $this->info('Enhance cleanup finished.');

        return self::SUCCESS;
    }

    private function cleanupExpiredCompletedJobs(bool $dryRun): array
    {
        $query = EnhanceJob::query()
            ->where('status', EnhanceJob::STATUS_COMPLETED)
            ->whereNotNull('expires_at')
            ->where('expires_at', '<=', now());

        return $this->cleanupJobs($query, $dryRun, 'expired', fn (): array => [
            'status' => EnhanceJob::STATUS_EXPIRED,
        ]);
    }

    private function cleanupFailedJobs(bool $dryRun, mixed $daysOverride): array
    {
        $days = $this->resolveDays($daysOverride, (int) config('enhance.lifecycle.failed_expires_after_days', 7));
        $cutoff = now()->subDays($days);

        $query = EnhanceJob::query()
            ->where('status', EnhanceJob::STATUS_FAILED)
            ->where(function (Builder $builder) use ($cutoff): void {
                $builder
                    ->where('finished_at', '<=', $cutoff)
                    ->orWhere(function (Builder $fallback) use ($cutoff): void {
                        $fallback->whereNull('finished_at')->where('created_at', '<=', $cutoff);
                    });
            });

        return $this->cleanupJobs($query, $dryRun, 'failed-expired');
    }

    private function cleanupSoftDeletedJobs(bool $dryRun, mixed $daysOverride): array
    {
        $days = $this->resolveDays($daysOverride, (int) config('enhance.lifecycle.deleted_file_grace_days', 1));
        $cutoff = now()->subDays($days);

        $query = EnhanceJob::withTrashed()
            ->whereNotNull('deleted_at')
            ->where('deleted_at', '<=', $cutoff);

        return $this->cleanupJobs($query, $dryRun, 'deleted-grace');
    }

    private function cleanupJobs(Builder $query, bool $dryRun, string $reason, ?callable $attributes = null): array
    {
        $result = ['jobs' => 0, 'files' => 0];
        $chunkSize = max(1, (int) config('enhance.lifecycle.cleanup_chunk_size', 100));

        $query->chunkById($chunkSize, function ($jobs) use (&$result, $dryRun, $reason, $attributes): void {
            foreach ($jobs as $job) {
                $result['jobs']++;
                $result['files'] += count($this->enhanceJobPaths($job));

                if ($dryRun) {
                    continue;
                }

                $deleteResult = $this->storage->deleteFilesForJob($job);
                $metadata = is_array($job->metadata) ? $job->metadata : [];

                $job->forceFill(array_merge(
                    $this->cleanupAttributesForJob($job),
                    $attributes ? $attributes($job) : [],
                    [
                        'metadata' => array_merge($metadata, [
                            'cleanup' => [
                                'files_removed_at' => now()->toIso8601String(),
                                'reason' => $reason,
                                'deleted' => $deleteResult['deleted'],
                            ],
                        ]),
                    ],
                ))->save();
            }
        });

        return $result;
    }

    private function scanOrphanedFiles(bool $dryRun): array
    {
        $disk = $this->storage->diskName();
        $knownPaths = array_fill_keys(array_map(
            static fn (string $path): string => ltrim($path, '/'),
            $this->storage->listKnownJobPaths(),
        ), true);
        $sample = [];
        $result = [
            'files' => 0,
            'deleted' => 0,
            'unsupported' => false,
            'sample' => [],
        ];

        foreach ($this->enhancePrefixes() as $prefix) {
            try {
                $files = Storage::disk($disk)->allFiles($prefix);
            } catch (Throwable) {
                $result['unsupported'] = true;

                return $result;
            }

            foreach ($files as $file) {
                $normalized = ltrim($file, '/');

                if (isset($knownPaths[$normalized])) {
                    continue;
                }

                $result['files']++;

                if (count($sample) < 20) {
                    $sample[] = $normalized;
                }

                if (! $dryRun && $this->storage->safeDelete($disk, $normalized)) {
                    $result['deleted']++;
                }
            }
        }

        $result['sample'] = $sample;

        return $result;
    }

    private function cleanupAttributesForJob(EnhanceJob $job): array
    {
        $attributes = [];

        if ($this->storage->isEnhancePath($job->source_path)) {
            $attributes['source_disk'] = null;
            $attributes['source_path'] = null;
            $attributes['source_hash'] = null;
        }

        if ($this->storage->isEnhancePath($job->output_path)) {
            $attributes['output_disk'] = null;
            $attributes['output_path'] = null;
            $attributes['output_hash'] = null;
            $attributes['output_width'] = null;
            $attributes['output_height'] = null;
            $attributes['output_filesize'] = null;
            $attributes['output_mime'] = null;
        }

        if ($this->storage->isEnhancePath($job->preview_path)) {
            $attributes['preview_disk'] = null;
            $attributes['preview_path'] = null;
        }

        return $attributes;
    }

    private function enhanceJobPaths(EnhanceJob $job): array
    {
        return array_values(array_filter([
            $this->storage->isEnhancePath($job->source_path) ? trim((string) $job->source_path) : null,
            $this->storage->isEnhancePath($job->output_path) ? trim((string) $job->output_path) : null,
            $this->storage->isEnhancePath($job->preview_path) ? trim((string) $job->preview_path) : null,
        ]));
    }

    private function enhancePrefixes(): array
    {
        return array_values(array_filter(array_unique(array_map(
            static fn (string $prefix): string => trim($prefix, '/'),
            [
                (string) config('enhance.source_prefix', 'enhance/sources'),
                (string) config('enhance.output_prefix', 'enhance/outputs'),
                (string) config('enhance.preview_prefix', 'enhance/previews'),
            ],
        ))));
    }

    private function resolveDays(mixed $daysOverride, int $default): int
    {
        if ($daysOverride === null || $daysOverride === '') {
            return max(0, $default);
        }

        return max(0, (int) $daysOverride);
    }
}